@payload-auth-link/react 0.2.6 → 0.4.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 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.
@@ -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. */
@@ -1 +1 @@
1
- {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.tsx"],"names":[],"mappings":"AAEA,OAAO,EAOL,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;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,QAAQ,EACR,eAA4C,EAC5C,SAAiB,EACjB,WAAuB,GACxB,EAAE,qBAAqB,GAAG,SAAS,CAiEnC;AAED,4EAA4E;AAC5E,wBAAgB,WAAW,IAAI,oBAAoB,CAMlD;AAED,0EAA0E;AAC1E,wBAAgB,WAAW,CAAC,GAAG,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAMhE"}
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
- const loadSession = useCallback(() => fetchSession(sessionEndpoint, fetchImpl), [sessionEndpoint, fetchImpl]);
55
- const refresh = useCallback(async () => {
56
- const current = await loadSession();
57
- setUser(current.user);
58
- setGuest(current.guest);
59
- setStatus(current.user === null ? 'anonymous' : 'authenticated');
60
- }, [loadSession]);
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
- void (async () => {
78
- const current = await loadSession();
79
- if (!cancelled) {
80
- setUser(current.user);
81
- setGuest(current.guest);
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
- cancelled = true;
123
+ mount.abort();
87
124
  };
88
- }, [initialUser, loadSession]);
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
  }
@@ -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;;;;;;;;GAQG;AACH,MAAM,UAAU,gBAAgB,CAAC,EAC/B,QAAQ,EACR,eAAe,GAAG,0BAA0B,EAC5C,SAAS,GAAG,KAAK,EACjB,WAAW,GAAG,SAAS,GACD;IACtB,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAqB,WAAW,IAAI,IAAI,CAAC,CAAA;IACzE,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAsB,IAAI,CAAC,CAAA;IAC7D,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAClC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,WAAW,CAAC,CAC/D,CAAA;IACD,qEAAqE;IACrE,6DAA6D;IAC7D,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC,CAAA;IAC1E,MAAM,WAAW,GAAG,WAAW,CAAC,WAAW,CAAC,CAAA;IAC5C,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;QAC9B,YAAY,CAAC,WAAW,CAAC,CAAA;QACzB,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;YAC9B,OAAO,CAAC,WAAW,CAAC,CAAA;YACpB,SAAS,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC,CAAA;QACnC,CAAC;IACH,CAAC;IAED,MAAM,WAAW,GAAG,WAAW,CAC7B,GAAG,EAAE,CAAC,YAAY,CAAC,eAAe,EAAE,SAAS,CAAC,EAC9C,CAAC,eAAe,EAAE,SAAS,CAAC,CAC7B,CAAA;IAED,MAAM,OAAO,GAAG,WAAW,CAAC,KAAK,IAAI,EAAE;QACrC,MAAM,OAAO,GAAG,MAAM,WAAW,EAAE,CAAA;QACnC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;QACrB,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;QACvB,SAAS,CAAC,OAAO,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,eAAe,CAAC,CAAA;IAClE,CAAC,EAAE,CAAC,WAAW,CAAC,CAAC,CAAA;IAEjB,MAAM,OAAO,GAAG,WAAW,CAAC,KAAK,IAAI,EAAE;QACrC,MAAM,OAAO,GAAG,MAAM,aAAa,CAAC,eAAe,EAAE,SAAS,CAAC,CAAA;QAC/D,IAAI,OAAO,EAAE,CAAC;YACZ,OAAO,CAAC,IAAI,CAAC,CAAA;YACb,QAAQ,CAAC,IAAI,CAAC,CAAA;YACd,SAAS,CAAC,WAAW,CAAC,CAAA;QACxB,CAAC;QACD,OAAO,OAAO,CAAA;IAChB,CAAC,EAAE,CAAC,eAAe,EAAE,SAAS,CAAC,CAAC,CAAA;IAEhC,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;YAC9B,OAAM;QACR,CAAC;QACD,IAAI,SAAS,GAAG,KAAK,CAAA;QACrB,oEAAoE;QACpE,qEAAqE;QACrE,KAAK,CAAC,KAAK,IAAI,EAAE;YACf,MAAM,OAAO,GAAG,MAAM,WAAW,EAAE,CAAA;YACnC,IAAI,CAAC,SAAS,EAAE,CAAC;gBACf,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;gBACrB,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;gBACvB,SAAS,CAAC,OAAO,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,eAAe,CAAC,CAAA;YAClE,CAAC;QACH,CAAC,CAAC,EAAE,CAAA;QACJ,OAAO,GAAG,EAAE;YACV,SAAS,GAAG,IAAI,CAAA;QAClB,CAAC,CAAA;IACH,CAAC,EAAE,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC,CAAA;IAE9B,MAAM,KAAK,GAAG,OAAO,CACnB,GAAG,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,EACjD,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,CACxC,CAAA;IACD,OAAO,KAAC,eAAe,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YAAG,QAAQ,GAA4B,CAAA;AACtF,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,WAAW;IACzB,MAAM,KAAK,GAAG,UAAU,CAAC,eAAe,CAAC,CAAA;IACzC,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAA;IACrE,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,WAAW,CAAC,GAAG,KAAwB;IACrD,MAAM,EAAE,IAAI,EAAE,GAAG,WAAW,EAAE,CAAA;IAC9B,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO,KAAK,CAAA;IACd,CAAC;IACD,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAA;AACzD,CAAC"}
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
@@ -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,GAC3B,OAAO,CAAC,eAAe,CAAC,CA6B1B;AAED;;;;;;GAMG;AACH,wBAAsB,aAAa,CACjC,eAAe,GAAE,MAAmC,EACpD,SAAS,GAAE,SAAiB,GAC3B,OAAO,CAAC,OAAO,CAAC,CAYlB"}
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 {
@@ -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;IAE5B,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;SACxC,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"}
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.2.6",
3
+ "version": "0.4.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.6.2"
40
+ "@payload-auth-link/core": "0.8.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
- const loadSession = useCallback(
118
- () => fetchSession(sessionEndpoint, fetchImpl),
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(async () => {
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
- void (async () => {
147
- const current = await loadSession()
148
- if (!cancelled) {
149
- setUser(current.user)
150
- setGuest(current.guest)
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
- cancelled = true
193
+ mount.abort()
156
194
  }
157
- }, [initialUser, loadSession])
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