@payload-auth-link/react 0.2.6 → 0.3.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/LICENSE +21 -0
- package/dist/provider.d.ts +4 -0
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +57 -20
- package/dist/provider.js.map +1 -1
- package/dist/session.d.ts +2 -2
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +3 -2
- package/dist/session.js.map +1 -1
- package/package.json +2 -2
- package/src/provider.tsx +58 -20
- package/src/session.ts +4 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 jellologic
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/provider.d.ts
CHANGED
|
@@ -43,6 +43,10 @@ export type AuthLinkProviderProps = {
|
|
|
43
43
|
* synced into context when its value changes; re-renders passing the
|
|
44
44
|
* same user as a fresh object change nothing, so a client-side sign-out
|
|
45
45
|
* is never undone by a stale snapshot. Refresh stays explicit.
|
|
46
|
+
*
|
|
47
|
+
* Loads are sequenced: only the newest read writes state (late,
|
|
48
|
+
* out-of-order results are dropped), and `signOut` cancels in-flight
|
|
49
|
+
* reads so none can flip the UI back to authenticated afterwards.
|
|
46
50
|
*/
|
|
47
51
|
export declare function AuthLinkProvider({ children, sessionEndpoint, fetchImpl, initialUser, }: AuthLinkProviderProps): ReactNode;
|
|
48
52
|
/** Read the auth context. Throws outside `AuthLinkProvider` — fail loud. */
|
package/dist/provider.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.tsx"],"names":[],"mappings":"AAEA,OAAO,
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.tsx"],"names":[],"mappings":"AAEA,OAAO,EAQL,KAAK,SAAS,EACf,MAAM,OAAO,CAAA;AACd,OAAO,EAEL,KAAK,SAAS,EACd,KAAK,YAAY,EACjB,KAAK,WAAW,EACjB,MAAM,yBAAyB,CAAA;AAGhC,2DAA2D;AAC3D,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,eAAe,GAAG,WAAW,CAAA;AAEtE,0EAA0E;AAC1E,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAAA;IACjC;;;;OAIG;IACH,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CAAA;IACnC,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAA;IAC/B,yEAAyE;IACzE,QAAQ,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IACrC;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAA;CACzC,CAAA;AAID,uEAAuE;AACvE,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;IAC5B,gEAAgE;IAChE,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC7C,wDAAwD;IACxD,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,GAAG,SAAS,CAAA;IAC1C;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,WAAW,GAAG,IAAI,GAAG,SAAS,CAAA;CACtD,CAAA;AA2BD;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,QAAQ,EACR,eAA4C,EAC5C,SAAiB,EACjB,WAAuB,GACxB,EAAE,qBAAqB,GAAG,SAAS,CAkGnC;AAED,4EAA4E;AAC5E,wBAAgB,WAAW,IAAI,oBAAoB,CAMlD;AAED,0EAA0E;AAC1E,wBAAgB,WAAW,CAAC,GAAG,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAMhE"}
|
package/dist/provider.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from 'react';
|
|
3
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from 'react';
|
|
4
4
|
import { defaultSessionEndpointPath, } from '@payload-auth-link/core';
|
|
5
5
|
import { fetchSession, revokeSession } from './session.js';
|
|
6
6
|
const AuthLinkContext = createContext(null);
|
|
@@ -35,6 +35,10 @@ function statusFor(user) {
|
|
|
35
35
|
* synced into context when its value changes; re-renders passing the
|
|
36
36
|
* same user as a fresh object change nothing, so a client-side sign-out
|
|
37
37
|
* is never undone by a stale snapshot. Refresh stays explicit.
|
|
38
|
+
*
|
|
39
|
+
* Loads are sequenced: only the newest read writes state (late,
|
|
40
|
+
* out-of-order results are dropped), and `signOut` cancels in-flight
|
|
41
|
+
* reads so none can flip the UI back to authenticated afterwards.
|
|
38
42
|
*/
|
|
39
43
|
export function AuthLinkProvider({ children, sessionEndpoint = defaultSessionEndpointPath, fetchImpl = fetch, initialUser = undefined, }) {
|
|
40
44
|
const [user, setUser] = useState(initialUser ?? null);
|
|
@@ -51,41 +55,74 @@ export function AuthLinkProvider({ children, sessionEndpoint = defaultSessionEnd
|
|
|
51
55
|
setStatus(statusFor(initialUser));
|
|
52
56
|
}
|
|
53
57
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
// Request sequencing. Every load (mount, refresh) takes a ticket;
|
|
59
|
+
// only the newest ticket may write state, so an older read that
|
|
60
|
+
// resolves late (out-of-order refreshes) is dropped. Sign-out bumps
|
|
61
|
+
// the generation and aborts in-flight loads, so a read that started
|
|
62
|
+
// before sign-out can never resurrect the signed-out session.
|
|
63
|
+
const generation = useRef(0);
|
|
64
|
+
const inFlight = useRef(new Set());
|
|
65
|
+
const abortInFlight = useCallback(() => {
|
|
66
|
+
const aborted = inFlight.current.size > 0;
|
|
67
|
+
for (const controller of inFlight.current) {
|
|
68
|
+
controller.abort();
|
|
69
|
+
}
|
|
70
|
+
inFlight.current.clear();
|
|
71
|
+
return aborted;
|
|
72
|
+
}, []);
|
|
73
|
+
const load = useCallback(async (controller = new AbortController()) => {
|
|
74
|
+
generation.current += 1;
|
|
75
|
+
const ticket = generation.current;
|
|
76
|
+
inFlight.current.add(controller);
|
|
77
|
+
try {
|
|
78
|
+
const current = await fetchSession(sessionEndpoint, fetchImpl, controller.signal);
|
|
79
|
+
if (ticket !== generation.current || controller.signal.aborted) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
setUser(current.user);
|
|
83
|
+
setGuest(current.guest);
|
|
84
|
+
setStatus(statusFor(current.user));
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
inFlight.current.delete(controller);
|
|
88
|
+
}
|
|
89
|
+
}, [sessionEndpoint, fetchImpl]);
|
|
90
|
+
const refresh = useCallback(() => load(), [load]);
|
|
61
91
|
const signOut = useCallback(async () => {
|
|
92
|
+
// Cancel reads that started before sign-out.
|
|
93
|
+
generation.current += 1;
|
|
94
|
+
const cancelledLoad = abortInFlight();
|
|
62
95
|
const revoked = await revokeSession(sessionEndpoint, fetchImpl);
|
|
63
96
|
if (revoked) {
|
|
97
|
+
// Also cancel reads started while the revocation was in flight.
|
|
98
|
+
generation.current += 1;
|
|
99
|
+
abortInFlight();
|
|
64
100
|
setUser(null);
|
|
65
101
|
setGuest(null);
|
|
66
102
|
setStatus('anonymous');
|
|
67
103
|
}
|
|
104
|
+
else if (cancelledLoad) {
|
|
105
|
+
// Revocation failed but a cancelled read left the state unknown:
|
|
106
|
+
// re-read so the UI reflects the (still live) session honestly.
|
|
107
|
+
await load();
|
|
108
|
+
}
|
|
68
109
|
return revoked;
|
|
69
|
-
}, [sessionEndpoint, fetchImpl]);
|
|
110
|
+
}, [sessionEndpoint, fetchImpl, abortInFlight, load]);
|
|
70
111
|
useEffect(() => {
|
|
71
112
|
if (initialUser !== undefined) {
|
|
72
113
|
return;
|
|
73
114
|
}
|
|
74
|
-
let cancelled = false;
|
|
75
115
|
// Mount-only initial load (the async boundary keeps this out of the
|
|
76
116
|
// sync-setState-in-effect rule); `refresh` stays the public re-read.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
setStatus(current.user === null ? 'anonymous' : 'authenticated');
|
|
83
|
-
}
|
|
84
|
-
})();
|
|
117
|
+
const mount = new AbortController();
|
|
118
|
+
void load(mount);
|
|
119
|
+
// Cancel only this mount read: the effect re-runs whenever the
|
|
120
|
+
// `initialUser` prop changes, and an explicit `refresh()` (the
|
|
121
|
+
// post-sign-in read) in flight at that moment must still land.
|
|
85
122
|
return () => {
|
|
86
|
-
|
|
123
|
+
mount.abort();
|
|
87
124
|
};
|
|
88
|
-
}, [initialUser,
|
|
125
|
+
}, [initialUser, load]);
|
|
89
126
|
const value = useMemo(() => ({ user, guest, status, refresh, signOut }), [user, guest, status, refresh, signOut]);
|
|
90
127
|
return _jsx(AuthLinkContext.Provider, { value: value, children: children });
|
|
91
128
|
}
|
package/dist/provider.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.js","sourceRoot":"","sources":["../src/provider.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ,OAAO,EACL,aAAa,EACb,WAAW,EACX,UAAU,EACV,SAAS,EACT,OAAO,EACP,QAAQ,GAET,MAAM,OAAO,CAAA;AACd,OAAO,EACL,0BAA0B,GAI3B,MAAM,yBAAyB,CAAA;AAChC,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAwB1D,MAAM,eAAe,GAAG,aAAa,CAA8B,IAAI,CAAC,CAAA;AAkBxE,yEAAyE;AACzE,SAAS,WAAW,CAAC,IAAoC;IACvD,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,OAAO,WAAW,CAAA;IACpB,CAAC;IACD,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;QAClB,OAAO,MAAM,CAAA;IACf,CAAC;IACD,kEAAkE;IAClE,gEAAgE;IAChE,OAAO,IAAI,CAAC,SAAS,CAAC;QACpB,IAAI,CAAC,EAAE;QACP,IAAI,CAAC,UAAU;QACf,IAAI,CAAC,KAAK;QACV,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE;QACtB,IAAI,CAAC,SAAS,IAAI,IAAI;QACtB,IAAI,CAAC,KAAK,IAAI,IAAI;QAClB,IAAI,CAAC,cAAc,IAAI,IAAI;KAC5B,CAAC,CAAA;AACJ,CAAC;AAED,SAAS,SAAS,CAAC,IAAwB;IACzC,OAAO,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,eAAe,CAAA;AACtD,CAAC;AAED
|
|
1
|
+
{"version":3,"file":"provider.js","sourceRoot":"","sources":["../src/provider.tsx"],"names":[],"mappings":"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"}
|
package/dist/session.d.ts
CHANGED
|
@@ -15,9 +15,9 @@ export type SessionSnapshot = {
|
|
|
15
15
|
* Read user AND guest from one session-endpoint response. The body
|
|
16
16
|
* is parsed exactly once; a guest body yields `user: null` plus the
|
|
17
17
|
* guest, a user body the reverse. Fails closed like
|
|
18
|
-
* {@link fetchSessionUser}.
|
|
18
|
+
* {@link fetchSessionUser}; an aborted `signal` reads as empty.
|
|
19
19
|
*/
|
|
20
|
-
export declare function fetchSession(sessionEndpoint?: string, fetchImpl?: HttpFetch): Promise<SessionSnapshot>;
|
|
20
|
+
export declare function fetchSession(sessionEndpoint?: string, fetchImpl?: HttpFetch, signal?: AbortSignal): Promise<SessionSnapshot>;
|
|
21
21
|
/**
|
|
22
22
|
* Revoke the current session through the plugin's session endpoint.
|
|
23
23
|
* Sends the CSRF header the endpoint requires. Returns `true` when the
|
package/dist/session.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,SAAS,EACd,KAAK,YAAY,EACjB,KAAK,WAAW,EACjB,MAAM,yBAAyB,CAAA;AAEhC;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,eAAe,GAAE,MAAmC,EACpD,SAAS,GAAE,SAAiB,GAC3B,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAe7B;AAED,yDAAyD;AACzD,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAAA;IACjC,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CAAA;CACpC,CAAA;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAChC,eAAe,GAAE,MAAmC,EACpD,SAAS,GAAE,SAAiB,
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,SAAS,EACd,KAAK,YAAY,EACjB,KAAK,WAAW,EACjB,MAAM,yBAAyB,CAAA;AAEhC;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,eAAe,GAAE,MAAmC,EACpD,SAAS,GAAE,SAAiB,GAC3B,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAe7B;AAED,yDAAyD;AACzD,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAAA;IACjC,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CAAA;CACpC,CAAA;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAChC,eAAe,GAAE,MAAmC,EACpD,SAAS,GAAE,SAAiB,EAC5B,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,eAAe,CAAC,CA8B1B;AAED;;;;;;GAMG;AACH,wBAAsB,aAAa,CACjC,eAAe,GAAE,MAAmC,EACpD,SAAS,GAAE,SAAiB,GAC3B,OAAO,CAAC,OAAO,CAAC,CAYlB"}
|
package/dist/session.js
CHANGED
|
@@ -26,9 +26,9 @@ export async function fetchSessionUser(sessionEndpoint = defaultSessionEndpointP
|
|
|
26
26
|
* Read user AND guest from one session-endpoint response. The body
|
|
27
27
|
* is parsed exactly once; a guest body yields `user: null` plus the
|
|
28
28
|
* guest, a user body the reverse. Fails closed like
|
|
29
|
-
* {@link fetchSessionUser}.
|
|
29
|
+
* {@link fetchSessionUser}; an aborted `signal` reads as empty.
|
|
30
30
|
*/
|
|
31
|
-
export async function fetchSession(sessionEndpoint = defaultSessionEndpointPath, fetchImpl = fetch) {
|
|
31
|
+
export async function fetchSession(sessionEndpoint = defaultSessionEndpointPath, fetchImpl = fetch, signal) {
|
|
32
32
|
const empty = { user: null, guest: null };
|
|
33
33
|
let response;
|
|
34
34
|
try {
|
|
@@ -37,6 +37,7 @@ export async function fetchSession(sessionEndpoint = defaultSessionEndpointPath,
|
|
|
37
37
|
credentials: 'same-origin',
|
|
38
38
|
cache: 'no-store',
|
|
39
39
|
headers: { accept: 'application/json' },
|
|
40
|
+
...(signal === undefined ? {} : { signal }),
|
|
40
41
|
});
|
|
41
42
|
}
|
|
42
43
|
catch {
|
package/dist/session.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,0BAA0B,EAC1B,QAAQ,EACR,iBAAiB,EACjB,oBAAoB,EACpB,gBAAgB,GAIjB,MAAM,yBAAyB,CAAA;AAEhC;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,kBAA0B,0BAA0B,EACpD,YAAuB,KAAK;IAE5B,IAAI,QAAkB,CAAA;IACtB,IAAI,CAAC;QACH,QAAQ,GAAG,MAAM,SAAS,CAAC,eAAe,EAAE;YAC1C,MAAM,EAAE,KAAK;YACb,WAAW,EAAE,aAAa;YAC1B,0DAA0D;YAC1D,mDAAmD;YACnD,KAAK,EAAE,UAAU;YACjB,OAAO,EAAE,EAAE,MAAM,EAAE,kBAAkB,EAAE;SACxC,CAAC,CAAA;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAA;IACb,CAAC;IACD,OAAO,oBAAoB,CAAC,QAAQ,CAAC,CAAA;AACvC,CAAC;AAQD;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,kBAA0B,0BAA0B,EACpD,YAAuB,KAAK;
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,0BAA0B,EAC1B,QAAQ,EACR,iBAAiB,EACjB,oBAAoB,EACpB,gBAAgB,GAIjB,MAAM,yBAAyB,CAAA;AAEhC;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,kBAA0B,0BAA0B,EACpD,YAAuB,KAAK;IAE5B,IAAI,QAAkB,CAAA;IACtB,IAAI,CAAC;QACH,QAAQ,GAAG,MAAM,SAAS,CAAC,eAAe,EAAE;YAC1C,MAAM,EAAE,KAAK;YACb,WAAW,EAAE,aAAa;YAC1B,0DAA0D;YAC1D,mDAAmD;YACnD,KAAK,EAAE,UAAU;YACjB,OAAO,EAAE,EAAE,MAAM,EAAE,kBAAkB,EAAE;SACxC,CAAC,CAAA;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAA;IACb,CAAC;IACD,OAAO,oBAAoB,CAAC,QAAQ,CAAC,CAAA;AACvC,CAAC;AAQD;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,kBAA0B,0BAA0B,EACpD,YAAuB,KAAK,EAC5B,MAAoB;IAEpB,MAAM,KAAK,GAAoB,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAA;IAC1D,IAAI,QAAkB,CAAA;IACtB,IAAI,CAAC;QACH,QAAQ,GAAG,MAAM,SAAS,CAAC,eAAe,EAAE;YAC1C,MAAM,EAAE,KAAK;YACb,WAAW,EAAE,aAAa;YAC1B,KAAK,EAAE,UAAU;YACjB,OAAO,EAAE,EAAE,MAAM,EAAE,kBAAkB,EAAE;YACvC,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC;SAC5C,CAAC,CAAA;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAA;IACd,CAAC;IACD,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,OAAO,KAAK,CAAA;IACd,CAAC;IACD,IAAI,IAAa,CAAA;IACjB,IAAI,CAAC;QACH,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAA;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAA;IACd,CAAC;IACD,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QACpB,OAAO,KAAK,CAAA;IACd,CAAC;IACD,OAAO;QACL,IAAI,EAAE,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC;QACjC,KAAK,EAAE,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,iBAAiB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI;KACjE,CAAA;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,kBAA0B,0BAA0B,EACpD,YAAuB,KAAK;IAE5B,IAAI,QAAkB,CAAA;IACtB,IAAI,CAAC;QACH,QAAQ,GAAG,MAAM,SAAS,CAAC,eAAe,EAAE;YAC1C,MAAM,EAAE,QAAQ;YAChB,WAAW,EAAE,aAAa;YAC1B,OAAO,EAAE,EAAE,CAAC,cAAc,CAAC,EAAE,GAAG,EAAE;SACnC,CAAC,CAAA;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAA;IACd,CAAC;IACD,OAAO,QAAQ,CAAC,EAAE,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,CAAA;AAC/C,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@payload-auth-link/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "payload-auth-link React UI: provider-agnostic sign-in components and user menu.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"clean": "rm -rf dist *.tsbuildinfo"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@payload-auth-link/core": "0.
|
|
40
|
+
"@payload-auth-link/core": "0.7.0"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"react": "^19.0.0"
|
package/src/provider.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
useContext,
|
|
7
7
|
useEffect,
|
|
8
8
|
useMemo,
|
|
9
|
+
useRef,
|
|
9
10
|
useState,
|
|
10
11
|
type ReactNode,
|
|
11
12
|
} from 'react'
|
|
@@ -90,6 +91,10 @@ function statusFor(user: SessionUser | null): AuthLinkStatus {
|
|
|
90
91
|
* synced into context when its value changes; re-renders passing the
|
|
91
92
|
* same user as a fresh object change nothing, so a client-side sign-out
|
|
92
93
|
* is never undone by a stale snapshot. Refresh stays explicit.
|
|
94
|
+
*
|
|
95
|
+
* Loads are sequenced: only the newest read writes state (late,
|
|
96
|
+
* out-of-order results are dropped), and `signOut` cancels in-flight
|
|
97
|
+
* reads so none can flip the UI back to authenticated afterwards.
|
|
93
98
|
*/
|
|
94
99
|
export function AuthLinkProvider({
|
|
95
100
|
children,
|
|
@@ -114,47 +119,80 @@ export function AuthLinkProvider({
|
|
|
114
119
|
}
|
|
115
120
|
}
|
|
116
121
|
|
|
117
|
-
|
|
118
|
-
|
|
122
|
+
// Request sequencing. Every load (mount, refresh) takes a ticket;
|
|
123
|
+
// only the newest ticket may write state, so an older read that
|
|
124
|
+
// resolves late (out-of-order refreshes) is dropped. Sign-out bumps
|
|
125
|
+
// the generation and aborts in-flight loads, so a read that started
|
|
126
|
+
// before sign-out can never resurrect the signed-out session.
|
|
127
|
+
const generation = useRef(0)
|
|
128
|
+
const inFlight = useRef(new Set<AbortController>())
|
|
129
|
+
|
|
130
|
+
const abortInFlight = useCallback((): boolean => {
|
|
131
|
+
const aborted = inFlight.current.size > 0
|
|
132
|
+
for (const controller of inFlight.current) {
|
|
133
|
+
controller.abort()
|
|
134
|
+
}
|
|
135
|
+
inFlight.current.clear()
|
|
136
|
+
return aborted
|
|
137
|
+
}, [])
|
|
138
|
+
|
|
139
|
+
const load = useCallback(
|
|
140
|
+
async (controller: AbortController = new AbortController()): Promise<void> => {
|
|
141
|
+
generation.current += 1
|
|
142
|
+
const ticket = generation.current
|
|
143
|
+
inFlight.current.add(controller)
|
|
144
|
+
try {
|
|
145
|
+
const current = await fetchSession(sessionEndpoint, fetchImpl, controller.signal)
|
|
146
|
+
if (ticket !== generation.current || controller.signal.aborted) {
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
setUser(current.user)
|
|
150
|
+
setGuest(current.guest)
|
|
151
|
+
setStatus(statusFor(current.user))
|
|
152
|
+
} finally {
|
|
153
|
+
inFlight.current.delete(controller)
|
|
154
|
+
}
|
|
155
|
+
},
|
|
119
156
|
[sessionEndpoint, fetchImpl]
|
|
120
157
|
)
|
|
121
158
|
|
|
122
|
-
const refresh = useCallback(
|
|
123
|
-
const current = await loadSession()
|
|
124
|
-
setUser(current.user)
|
|
125
|
-
setGuest(current.guest)
|
|
126
|
-
setStatus(current.user === null ? 'anonymous' : 'authenticated')
|
|
127
|
-
}, [loadSession])
|
|
159
|
+
const refresh = useCallback(() => load(), [load])
|
|
128
160
|
|
|
129
161
|
const signOut = useCallback(async () => {
|
|
162
|
+
// Cancel reads that started before sign-out.
|
|
163
|
+
generation.current += 1
|
|
164
|
+
const cancelledLoad = abortInFlight()
|
|
130
165
|
const revoked = await revokeSession(sessionEndpoint, fetchImpl)
|
|
131
166
|
if (revoked) {
|
|
167
|
+
// Also cancel reads started while the revocation was in flight.
|
|
168
|
+
generation.current += 1
|
|
169
|
+
abortInFlight()
|
|
132
170
|
setUser(null)
|
|
133
171
|
setGuest(null)
|
|
134
172
|
setStatus('anonymous')
|
|
173
|
+
} else if (cancelledLoad) {
|
|
174
|
+
// Revocation failed but a cancelled read left the state unknown:
|
|
175
|
+
// re-read so the UI reflects the (still live) session honestly.
|
|
176
|
+
await load()
|
|
135
177
|
}
|
|
136
178
|
return revoked
|
|
137
|
-
}, [sessionEndpoint, fetchImpl])
|
|
179
|
+
}, [sessionEndpoint, fetchImpl, abortInFlight, load])
|
|
138
180
|
|
|
139
181
|
useEffect(() => {
|
|
140
182
|
if (initialUser !== undefined) {
|
|
141
183
|
return
|
|
142
184
|
}
|
|
143
|
-
let cancelled = false
|
|
144
185
|
// Mount-only initial load (the async boundary keeps this out of the
|
|
145
186
|
// sync-setState-in-effect rule); `refresh` stays the public re-read.
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
setStatus(current.user === null ? 'anonymous' : 'authenticated')
|
|
152
|
-
}
|
|
153
|
-
})()
|
|
187
|
+
const mount = new AbortController()
|
|
188
|
+
void load(mount)
|
|
189
|
+
// Cancel only this mount read: the effect re-runs whenever the
|
|
190
|
+
// `initialUser` prop changes, and an explicit `refresh()` (the
|
|
191
|
+
// post-sign-in read) in flight at that moment must still land.
|
|
154
192
|
return () => {
|
|
155
|
-
|
|
193
|
+
mount.abort()
|
|
156
194
|
}
|
|
157
|
-
}, [initialUser,
|
|
195
|
+
}, [initialUser, load])
|
|
158
196
|
|
|
159
197
|
const value = useMemo<AuthLinkContextValue>(
|
|
160
198
|
() => ({ user, guest, status, refresh, signOut }),
|
package/src/session.ts
CHANGED
|
@@ -46,11 +46,12 @@ export type SessionSnapshot = {
|
|
|
46
46
|
* Read user AND guest from one session-endpoint response. The body
|
|
47
47
|
* is parsed exactly once; a guest body yields `user: null` plus the
|
|
48
48
|
* guest, a user body the reverse. Fails closed like
|
|
49
|
-
* {@link fetchSessionUser}.
|
|
49
|
+
* {@link fetchSessionUser}; an aborted `signal` reads as empty.
|
|
50
50
|
*/
|
|
51
51
|
export async function fetchSession(
|
|
52
52
|
sessionEndpoint: string = defaultSessionEndpointPath,
|
|
53
|
-
fetchImpl: HttpFetch = fetch
|
|
53
|
+
fetchImpl: HttpFetch = fetch,
|
|
54
|
+
signal?: AbortSignal
|
|
54
55
|
): Promise<SessionSnapshot> {
|
|
55
56
|
const empty: SessionSnapshot = { user: null, guest: null }
|
|
56
57
|
let response: Response
|
|
@@ -60,6 +61,7 @@ export async function fetchSession(
|
|
|
60
61
|
credentials: 'same-origin',
|
|
61
62
|
cache: 'no-store',
|
|
62
63
|
headers: { accept: 'application/json' },
|
|
64
|
+
...(signal === undefined ? {} : { signal }),
|
|
63
65
|
})
|
|
64
66
|
} catch {
|
|
65
67
|
return empty
|