@payload-auth-link/react 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,7 +16,7 @@ ESM-only, Node 20.9 or later.
16
16
 
17
17
  ## What it provides
18
18
 
19
- - `AuthLinkProvider`, `useAuthLink`, `useHasRoles`: session context, hydrated from the server through `initialUser`
19
+ - `AuthLinkProvider`, `useAuthLink`, `useHasRoles`: session context (`user`, anonymous `guest`, `mfaRequired` step-up signal), hydrated from the server through `initialUser`
20
20
  - `SignInButton`, `SignOutButton`, `UserMenu`: unstyled primitives
21
21
  - `RequireRoles`: hides UI by role (never use it to gate content; enforce on the server)
22
22
 
package/dist/index.d.ts CHANGED
@@ -8,5 +8,6 @@ export { RequireRoles, SignInButton, SignOutButton, UserMenu } from './component
8
8
  export type { RequireRolesProps, SignInButtonProps, SignOutButtonProps, UserMenuProps, } from './components.js';
9
9
  export { AuthLinkProvider, useAuthLink, useHasRoles } from './provider.js';
10
10
  export type { AuthLinkContextValue, AuthLinkProviderProps, AuthLinkStatus } from './provider.js';
11
- export { fetchSessionUser, revokeSession } from './session.js';
11
+ export { fetchSession, fetchSessionUser, revokeSession } from './session.js';
12
+ export type { SessionSnapshot } from './session.js';
12
13
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,eAAO,MAAM,WAAW,6BAA6B,CAAA;AAErD,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AACrF,YAAY,EACV,iBAAiB,EACjB,iBAAiB,EACjB,kBAAkB,EAClB,aAAa,GACd,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAC1E,YAAY,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAChG,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,eAAO,MAAM,WAAW,6BAA6B,CAAA;AAErD,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AACrF,YAAY,EACV,iBAAiB,EACjB,iBAAiB,EACjB,kBAAkB,EAClB,aAAa,GACd,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAC1E,YAAY,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAChG,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAC5E,YAAY,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA"}
package/dist/index.js CHANGED
@@ -6,5 +6,5 @@
6
6
  export const packageName = '@payload-auth-link/react';
7
7
  export { RequireRoles, SignInButton, SignOutButton, UserMenu } from './components.js';
8
8
  export { AuthLinkProvider, useAuthLink, useHasRoles } from './provider.js';
9
- export { fetchSessionUser, revokeSession } from './session.js';
9
+ export { fetchSession, fetchSessionUser, revokeSession } from './session.js';
10
10
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,0BAA0B,CAAA;AAErD,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAOrF,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAE1E,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,0BAA0B,CAAA;AAErD,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAOrF,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAE1E,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA"}
@@ -1,10 +1,16 @@
1
1
  import { type ReactNode } from 'react';
2
- import { type HttpFetch, type SessionUser } from '@payload-auth-link/core';
2
+ import { type HttpFetch, type SessionGuest, type SessionUser } from '@payload-auth-link/core';
3
3
  /** Session state: unknown until the first load settles. */
4
4
  export type AuthLinkStatus = 'loading' | 'authenticated' | 'anonymous';
5
5
  /** What `useAuthLink` returns: user, status, refresh, honest sign-out. */
6
6
  export type AuthLinkContextValue = {
7
7
  readonly user: SessionUser | null;
8
+ /**
9
+ * The anonymous guest principal (CAP-10), present instead of a
10
+ * user while the provider session is anonymous. Never carries
11
+ * roles or access — a stable key for anonymous app state only.
12
+ */
13
+ readonly guest: SessionGuest | null;
8
14
  readonly status: AuthLinkStatus;
9
15
  /** Re-read the session endpoint (call after sign-in or role changes). */
10
16
  readonly refresh: () => Promise<void>;
@@ -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,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,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;AAiBD;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,QAAQ,EACR,eAA4C,EAC5C,SAAiB,EACjB,WAAuB,GACxB,EAAE,qBAAqB,GAAG,SAAS,CA6DnC;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,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"}
package/dist/provider.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from 'react';
4
4
  import { defaultSessionEndpointPath, } from '@payload-auth-link/core';
5
- import { fetchSessionUser, revokeSession } from './session.js';
5
+ import { fetchSession, revokeSession } from './session.js';
6
6
  const AuthLinkContext = createContext(null);
7
7
  /** Value identity for hydration snapshots (fresh objects, same user). */
8
8
  function snapshotKey(user) {
@@ -12,7 +12,17 @@ function snapshotKey(user) {
12
12
  if (user === null) {
13
13
  return 'null';
14
14
  }
15
- return JSON.stringify([user.id, user.collection, user.email, [...user.roles].sort()]);
15
+ // PORT-4 facts are part of the value: a re-run loader reporting a
16
+ // new actor/assurance/tenant for the same user must still sync.
17
+ return JSON.stringify([
18
+ user.id,
19
+ user.collection,
20
+ user.email,
21
+ [...user.roles].sort(),
22
+ user.assurance ?? null,
23
+ user.actor ?? null,
24
+ user.organizationId ?? null,
25
+ ]);
16
26
  }
17
27
  function statusFor(user) {
18
28
  return user === null ? 'anonymous' : 'authenticated';
@@ -28,6 +38,7 @@ function statusFor(user) {
28
38
  */
29
39
  export function AuthLinkProvider({ children, sessionEndpoint = defaultSessionEndpointPath, fetchImpl = fetch, initialUser = undefined, }) {
30
40
  const [user, setUser] = useState(initialUser ?? null);
41
+ const [guest, setGuest] = useState(null);
31
42
  const [status, setStatus] = useState(initialUser === undefined ? 'loading' : statusFor(initialUser));
32
43
  // React's "adjust state when a prop changes" pattern (during render,
33
44
  // not in an effect): no stale frame, no set-state-in-effect.
@@ -40,16 +51,18 @@ export function AuthLinkProvider({ children, sessionEndpoint = defaultSessionEnd
40
51
  setStatus(statusFor(initialUser));
41
52
  }
42
53
  }
43
- const loadSession = useCallback(() => fetchSessionUser(sessionEndpoint, fetchImpl), [sessionEndpoint, fetchImpl]);
54
+ const loadSession = useCallback(() => fetchSession(sessionEndpoint, fetchImpl), [sessionEndpoint, fetchImpl]);
44
55
  const refresh = useCallback(async () => {
45
56
  const current = await loadSession();
46
- setUser(current);
47
- setStatus(current === null ? 'anonymous' : 'authenticated');
57
+ setUser(current.user);
58
+ setGuest(current.guest);
59
+ setStatus(current.user === null ? 'anonymous' : 'authenticated');
48
60
  }, [loadSession]);
49
61
  const signOut = useCallback(async () => {
50
62
  const revoked = await revokeSession(sessionEndpoint, fetchImpl);
51
63
  if (revoked) {
52
64
  setUser(null);
65
+ setGuest(null);
53
66
  setStatus('anonymous');
54
67
  }
55
68
  return revoked;
@@ -64,15 +77,16 @@ export function AuthLinkProvider({ children, sessionEndpoint = defaultSessionEnd
64
77
  void (async () => {
65
78
  const current = await loadSession();
66
79
  if (!cancelled) {
67
- setUser(current);
68
- setStatus(current === null ? 'anonymous' : 'authenticated');
80
+ setUser(current.user);
81
+ setGuest(current.guest);
82
+ setStatus(current.user === null ? 'anonymous' : 'authenticated');
69
83
  }
70
84
  })();
71
85
  return () => {
72
86
  cancelled = true;
73
87
  };
74
88
  }, [initialUser, loadSession]);
75
- const value = useMemo(() => ({ user, status, refresh, signOut }), [user, status, refresh, signOut]);
89
+ const value = useMemo(() => ({ user, guest, status, refresh, signOut }), [user, guest, status, refresh, signOut]);
76
90
  return _jsx(AuthLinkContext.Provider, { value: value, children: children });
77
91
  }
78
92
  /** Read the auth context. Throws outside `AuthLinkProvider` — fail loud. */
@@ -1 +1 @@
1
- {"version":3,"file":"provider.js","sourceRoot":"","sources":["../src/provider.tsx"],"names":[],"mappings":"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"}
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"}
package/dist/session.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type HttpFetch, type SessionUser } from '@payload-auth-link/core';
1
+ import { type HttpFetch, type SessionGuest, type SessionUser } from '@payload-auth-link/core';
2
2
  /**
3
3
  * Read the current session user from the plugin's session endpoint
4
4
  * (`{ user }` envelope). Fails closed: transport errors, non-2xx
@@ -6,6 +6,18 @@ import { type HttpFetch, type SessionUser } from '@payload-auth-link/core';
6
6
  * never an exception and never a partial user.
7
7
  */
8
8
  export declare function fetchSessionUser(sessionEndpoint?: string, fetchImpl?: HttpFetch): Promise<SessionUser | null>;
9
+ /** Both halves of one session-endpoint read (CAP-10). */
10
+ export type SessionSnapshot = {
11
+ readonly user: SessionUser | null;
12
+ readonly guest: SessionGuest | null;
13
+ };
14
+ /**
15
+ * Read user AND guest from one session-endpoint response. The body
16
+ * is parsed exactly once; a guest body yields `user: null` plus the
17
+ * guest, a user body the reverse. Fails closed like
18
+ * {@link fetchSessionUser}.
19
+ */
20
+ export declare function fetchSession(sessionEndpoint?: string, fetchImpl?: HttpFetch): Promise<SessionSnapshot>;
9
21
  /**
10
22
  * Revoke the current session through the plugin's session endpoint.
11
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,EAIL,KAAK,SAAS,EACd,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;;;;;;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,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"}
package/dist/session.js CHANGED
@@ -1,4 +1,4 @@
1
- import { csrfHeaderName, defaultSessionEndpointPath, parseSessionResponse, } from '@payload-auth-link/core';
1
+ import { csrfHeaderName, defaultSessionEndpointPath, isRecord, parseSessionGuest, parseSessionResponse, parseSessionUser, } from '@payload-auth-link/core';
2
2
  /**
3
3
  * Read the current session user from the plugin's session endpoint
4
4
  * (`{ user }` envelope). Fails closed: transport errors, non-2xx
@@ -22,6 +22,44 @@ export async function fetchSessionUser(sessionEndpoint = defaultSessionEndpointP
22
22
  }
23
23
  return parseSessionResponse(response);
24
24
  }
25
+ /**
26
+ * Read user AND guest from one session-endpoint response. The body
27
+ * is parsed exactly once; a guest body yields `user: null` plus the
28
+ * guest, a user body the reverse. Fails closed like
29
+ * {@link fetchSessionUser}.
30
+ */
31
+ export async function fetchSession(sessionEndpoint = defaultSessionEndpointPath, fetchImpl = fetch) {
32
+ const empty = { user: null, guest: null };
33
+ let response;
34
+ try {
35
+ response = await fetchImpl(sessionEndpoint, {
36
+ method: 'GET',
37
+ credentials: 'same-origin',
38
+ cache: 'no-store',
39
+ headers: { accept: 'application/json' },
40
+ });
41
+ }
42
+ catch {
43
+ return empty;
44
+ }
45
+ if (!response.ok) {
46
+ return empty;
47
+ }
48
+ let body;
49
+ try {
50
+ body = await response.json();
51
+ }
52
+ catch {
53
+ return empty;
54
+ }
55
+ if (!isRecord(body)) {
56
+ return empty;
57
+ }
58
+ return {
59
+ user: parseSessionUser(body.user),
60
+ guest: body.user === null ? parseSessionGuest(body.guest) : null,
61
+ };
62
+ }
25
63
  /**
26
64
  * Revoke the current session through the plugin's session endpoint.
27
65
  * Sends the CSRF header the endpoint requires. Returns `true` when the
@@ -1 +1 @@
1
- {"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,0BAA0B,EAC1B,oBAAoB,GAGrB,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;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;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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@payload-auth-link/react",
3
- "version": "0.1.1",
3
+ "version": "0.2.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.1.1"
40
+ "@payload-auth-link/core": "0.2.0"
41
41
  },
42
42
  "peerDependencies": {
43
43
  "react": "^19.0.0"
package/src/index.ts CHANGED
@@ -14,4 +14,5 @@ export type {
14
14
  } from './components.js'
15
15
  export { AuthLinkProvider, useAuthLink, useHasRoles } from './provider.js'
16
16
  export type { AuthLinkContextValue, AuthLinkProviderProps, AuthLinkStatus } from './provider.js'
17
- export { fetchSessionUser, revokeSession } from './session.js'
17
+ export { fetchSession, fetchSessionUser, revokeSession } from './session.js'
18
+ export type { SessionSnapshot } from './session.js'
package/src/provider.tsx CHANGED
@@ -12,9 +12,10 @@ import {
12
12
  import {
13
13
  defaultSessionEndpointPath,
14
14
  type HttpFetch,
15
+ type SessionGuest,
15
16
  type SessionUser,
16
17
  } from '@payload-auth-link/core'
17
- import { fetchSessionUser, revokeSession } from './session.js'
18
+ import { fetchSession, revokeSession } from './session.js'
18
19
 
19
20
  /** Session state: unknown until the first load settles. */
20
21
  export type AuthLinkStatus = 'loading' | 'authenticated' | 'anonymous'
@@ -22,6 +23,12 @@ export type AuthLinkStatus = 'loading' | 'authenticated' | 'anonymous'
22
23
  /** What `useAuthLink` returns: user, status, refresh, honest sign-out. */
23
24
  export type AuthLinkContextValue = {
24
25
  readonly user: SessionUser | null
26
+ /**
27
+ * The anonymous guest principal (CAP-10), present instead of a
28
+ * user while the provider session is anonymous. Never carries
29
+ * roles or access — a stable key for anonymous app state only.
30
+ */
31
+ readonly guest: SessionGuest | null
25
32
  readonly status: AuthLinkStatus
26
33
  /** Re-read the session endpoint (call after sign-in or role changes). */
27
34
  readonly refresh: () => Promise<void>
@@ -58,7 +65,17 @@ function snapshotKey(user: SessionUser | null | undefined): string {
58
65
  if (user === null) {
59
66
  return 'null'
60
67
  }
61
- return JSON.stringify([user.id, user.collection, user.email, [...user.roles].sort()])
68
+ // PORT-4 facts are part of the value: a re-run loader reporting a
69
+ // new actor/assurance/tenant for the same user must still sync.
70
+ return JSON.stringify([
71
+ user.id,
72
+ user.collection,
73
+ user.email,
74
+ [...user.roles].sort(),
75
+ user.assurance ?? null,
76
+ user.actor ?? null,
77
+ user.organizationId ?? null,
78
+ ])
62
79
  }
63
80
 
64
81
  function statusFor(user: SessionUser | null): AuthLinkStatus {
@@ -81,6 +98,7 @@ export function AuthLinkProvider({
81
98
  initialUser = undefined,
82
99
  }: AuthLinkProviderProps): ReactNode {
83
100
  const [user, setUser] = useState<SessionUser | null>(initialUser ?? null)
101
+ const [guest, setGuest] = useState<SessionGuest | null>(null)
84
102
  const [status, setStatus] = useState<AuthLinkStatus>(
85
103
  initialUser === undefined ? 'loading' : statusFor(initialUser)
86
104
  )
@@ -97,20 +115,22 @@ export function AuthLinkProvider({
97
115
  }
98
116
 
99
117
  const loadSession = useCallback(
100
- () => fetchSessionUser(sessionEndpoint, fetchImpl),
118
+ () => fetchSession(sessionEndpoint, fetchImpl),
101
119
  [sessionEndpoint, fetchImpl]
102
120
  )
103
121
 
104
122
  const refresh = useCallback(async () => {
105
123
  const current = await loadSession()
106
- setUser(current)
107
- setStatus(current === null ? 'anonymous' : 'authenticated')
124
+ setUser(current.user)
125
+ setGuest(current.guest)
126
+ setStatus(current.user === null ? 'anonymous' : 'authenticated')
108
127
  }, [loadSession])
109
128
 
110
129
  const signOut = useCallback(async () => {
111
130
  const revoked = await revokeSession(sessionEndpoint, fetchImpl)
112
131
  if (revoked) {
113
132
  setUser(null)
133
+ setGuest(null)
114
134
  setStatus('anonymous')
115
135
  }
116
136
  return revoked
@@ -126,8 +146,9 @@ export function AuthLinkProvider({
126
146
  void (async () => {
127
147
  const current = await loadSession()
128
148
  if (!cancelled) {
129
- setUser(current)
130
- setStatus(current === null ? 'anonymous' : 'authenticated')
149
+ setUser(current.user)
150
+ setGuest(current.guest)
151
+ setStatus(current.user === null ? 'anonymous' : 'authenticated')
131
152
  }
132
153
  })()
133
154
  return () => {
@@ -136,8 +157,8 @@ export function AuthLinkProvider({
136
157
  }, [initialUser, loadSession])
137
158
 
138
159
  const value = useMemo<AuthLinkContextValue>(
139
- () => ({ user, status, refresh, signOut }),
140
- [user, status, refresh, signOut]
160
+ () => ({ user, guest, status, refresh, signOut }),
161
+ [user, guest, status, refresh, signOut]
141
162
  )
142
163
  return <AuthLinkContext.Provider value={value}>{children}</AuthLinkContext.Provider>
143
164
  }
package/src/session.ts CHANGED
@@ -1,8 +1,12 @@
1
1
  import {
2
2
  csrfHeaderName,
3
3
  defaultSessionEndpointPath,
4
+ isRecord,
5
+ parseSessionGuest,
4
6
  parseSessionResponse,
7
+ parseSessionUser,
5
8
  type HttpFetch,
9
+ type SessionGuest,
6
10
  type SessionUser,
7
11
  } from '@payload-auth-link/core'
8
12
 
@@ -32,6 +36,52 @@ export async function fetchSessionUser(
32
36
  return parseSessionResponse(response)
33
37
  }
34
38
 
39
+ /** Both halves of one session-endpoint read (CAP-10). */
40
+ export type SessionSnapshot = {
41
+ readonly user: SessionUser | null
42
+ readonly guest: SessionGuest | null
43
+ }
44
+
45
+ /**
46
+ * Read user AND guest from one session-endpoint response. The body
47
+ * is parsed exactly once; a guest body yields `user: null` plus the
48
+ * guest, a user body the reverse. Fails closed like
49
+ * {@link fetchSessionUser}.
50
+ */
51
+ export async function fetchSession(
52
+ sessionEndpoint: string = defaultSessionEndpointPath,
53
+ fetchImpl: HttpFetch = fetch
54
+ ): Promise<SessionSnapshot> {
55
+ const empty: SessionSnapshot = { user: null, guest: null }
56
+ let response: Response
57
+ try {
58
+ response = await fetchImpl(sessionEndpoint, {
59
+ method: 'GET',
60
+ credentials: 'same-origin',
61
+ cache: 'no-store',
62
+ headers: { accept: 'application/json' },
63
+ })
64
+ } catch {
65
+ return empty
66
+ }
67
+ if (!response.ok) {
68
+ return empty
69
+ }
70
+ let body: unknown
71
+ try {
72
+ body = await response.json()
73
+ } catch {
74
+ return empty
75
+ }
76
+ if (!isRecord(body)) {
77
+ return empty
78
+ }
79
+ return {
80
+ user: parseSessionUser(body.user),
81
+ guest: body.user === null ? parseSessionGuest(body.guest) : null,
82
+ }
83
+ }
84
+
35
85
  /**
36
86
  * Revoke the current session through the plugin's session endpoint.
37
87
  * Sends the CSRF header the endpoint requires. Returns `true` when the