@nexauthxyz/react 0.1.2

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.
Files changed (42) hide show
  1. package/dist/Authenticated.d.ts +4 -0
  2. package/dist/Authenticated.d.ts.map +1 -0
  3. package/dist/Authenticated.js +7 -0
  4. package/dist/Authenticated.js.map +1 -0
  5. package/dist/NexAuthCallback.d.ts +4 -0
  6. package/dist/NexAuthCallback.d.ts.map +1 -0
  7. package/dist/NexAuthCallback.js +45 -0
  8. package/dist/NexAuthCallback.js.map +1 -0
  9. package/dist/NexAuthProvider.d.ts +4 -0
  10. package/dist/NexAuthProvider.d.ts.map +1 -0
  11. package/dist/NexAuthProvider.js +321 -0
  12. package/dist/NexAuthProvider.js.map +1 -0
  13. package/dist/RequireAuth.d.ts +4 -0
  14. package/dist/RequireAuth.d.ts.map +1 -0
  15. package/dist/RequireAuth.js +39 -0
  16. package/dist/RequireAuth.js.map +1 -0
  17. package/dist/Unauthenticated.d.ts +4 -0
  18. package/dist/Unauthenticated.d.ts.map +1 -0
  19. package/dist/Unauthenticated.js +7 -0
  20. package/dist/Unauthenticated.js.map +1 -0
  21. package/dist/context.d.ts +3 -0
  22. package/dist/context.d.ts.map +1 -0
  23. package/dist/context.js +3 -0
  24. package/dist/context.js.map +1 -0
  25. package/dist/hooks.d.ts +3 -0
  26. package/dist/hooks.d.ts.map +1 -0
  27. package/dist/hooks.js +10 -0
  28. package/dist/hooks.js.map +1 -0
  29. package/dist/index.d.ts +11 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +9 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/tab-sync.d.ts +15 -0
  34. package/dist/tab-sync.d.ts.map +1 -0
  35. package/dist/tab-sync.js +105 -0
  36. package/dist/tab-sync.js.map +1 -0
  37. package/dist/types.d.ts +87 -0
  38. package/dist/types.d.ts.map +1 -0
  39. package/dist/types.js +2 -0
  40. package/dist/types.js.map +1 -0
  41. package/package.json +65 -0
  42. package/readme.md +400 -0
@@ -0,0 +1,105 @@
1
+ const STORAGE_EVENT_PREFIX = 'nexauth:tab-sync';
2
+ function isTabEvent(value) {
3
+ if (typeof value !== 'object' || value === null) {
4
+ return false;
5
+ }
6
+ const record = value;
7
+ return (record.type === 'logout' &&
8
+ typeof record.source === 'string' &&
9
+ typeof record.timestamp === 'number');
10
+ }
11
+ function createSourceId() {
12
+ if (globalThis.crypto && typeof globalThis.crypto.randomUUID === 'function') {
13
+ return globalThis.crypto.randomUUID();
14
+ }
15
+ return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
16
+ }
17
+ export class NexAuthTabSync {
18
+ source = createSourceId();
19
+ channelName;
20
+ storageKey;
21
+ onEvent;
22
+ channel;
23
+ constructor(channelName, onEvent) {
24
+ this.channelName = channelName;
25
+ this.storageKey = `${STORAGE_EVENT_PREFIX}:${channelName}`;
26
+ this.onEvent = onEvent;
27
+ if (typeof globalThis.BroadcastChannel === 'function') {
28
+ this.channel = new BroadcastChannel(channelName);
29
+ this.channel.addEventListener('message', this.handleBroadcastMessage);
30
+ }
31
+ else {
32
+ this.channel = null;
33
+ }
34
+ if (typeof window !== 'undefined') {
35
+ window.addEventListener('storage', this.handleStorageEvent);
36
+ }
37
+ }
38
+ publishLogout() {
39
+ const event = {
40
+ source: this.source,
41
+ type: 'logout',
42
+ timestamp: Date.now(),
43
+ };
44
+ if (this.channel) {
45
+ try {
46
+ this.channel.postMessage(event);
47
+ return;
48
+ }
49
+ catch {
50
+ /*
51
+ * Fall through to the localStorage transport.
52
+ */
53
+ }
54
+ }
55
+ if (typeof window === 'undefined' || window.localStorage === undefined) {
56
+ return;
57
+ }
58
+ try {
59
+ window.localStorage.setItem(this.storageKey, JSON.stringify(event));
60
+ /*
61
+ * The storage event is sent to other tabs only. Removing the
62
+ * temporary value keeps localStorage free of stale SDK data.
63
+ */
64
+ window.localStorage.removeItem(this.storageKey);
65
+ }
66
+ catch {
67
+ /*
68
+ * Cross-tab synchronization is best-effort. Authentication
69
+ * and logout must still work when localStorage is unavailable.
70
+ */
71
+ }
72
+ }
73
+ close() {
74
+ if (this.channel) {
75
+ this.channel.removeEventListener('message', this.handleBroadcastMessage);
76
+ this.channel.close();
77
+ }
78
+ if (typeof window !== 'undefined') {
79
+ window.removeEventListener('storage', this.handleStorageEvent);
80
+ }
81
+ }
82
+ handleBroadcastMessage = (message) => {
83
+ this.receive(message.data);
84
+ };
85
+ handleStorageEvent = (event) => {
86
+ if (event.key !== this.storageKey || event.newValue === null) {
87
+ return;
88
+ }
89
+ try {
90
+ this.receive(JSON.parse(event.newValue));
91
+ }
92
+ catch {
93
+ /*
94
+ * Ignore malformed cross-tab events.
95
+ */
96
+ }
97
+ };
98
+ receive(value) {
99
+ if (!isTabEvent(value) || value.source === this.source) {
100
+ return;
101
+ }
102
+ this.onEvent(value.type);
103
+ }
104
+ }
105
+ //# sourceMappingURL=tab-sync.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tab-sync.js","sourceRoot":"","sources":["../src/tab-sync.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,87 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { AuthenticationStatus, AuthorizationCallbackInput, AuthorizationCallbackResult, LoginOptions, LogoutOptions, NexAuthBrowserClient, NexAuthBrowserClientOptions, NexAuthError, NexAuthSession, NexAuthUser } from '@nexauthxyz/browser';
3
+ export type NexAuthReactClient = Pick<NexAuthBrowserClient, 'getSession' | 'login' | 'handleCallback' | 'logout' | 'clearLocalSession'>;
4
+ export interface NexAuthProviderProps {
5
+ readonly children: ReactNode;
6
+ readonly options: NexAuthBrowserClientOptions;
7
+ /**
8
+ * Synchronize logout across tabs on the same origin.
9
+ *
10
+ * Defaults to true.
11
+ */
12
+ readonly syncTabs?: boolean;
13
+ /**
14
+ * Optional BroadcastChannel name.
15
+ *
16
+ * Defaults to a client-specific NexAuth channel.
17
+ */
18
+ readonly tabSyncChannel?: string;
19
+ /**
20
+ * Optional compatible client instance.
21
+ *
22
+ * Useful for testing or advanced dependency injection.
23
+ */
24
+ readonly client?: NexAuthReactClient;
25
+ readonly onError?: (error: NexAuthError) => void;
26
+ }
27
+ export interface NexAuthContextValue {
28
+ readonly status: AuthenticationStatus;
29
+ readonly session: NexAuthSession | null;
30
+ readonly user: NexAuthUser | null;
31
+ readonly accessToken: string | null;
32
+ readonly error: NexAuthError | null;
33
+ readonly isLoading: boolean;
34
+ readonly isAuthenticated: boolean;
35
+ login(options?: LoginOptions): Promise<void>;
36
+ handleCallback(input?: AuthorizationCallbackInput): Promise<AuthorizationCallbackResult>;
37
+ logout(options?: LogoutOptions): Promise<void>;
38
+ /**
39
+ * Re-read the session currently held by the browser SDK.
40
+ */
41
+ refreshSession(): NexAuthSession | null;
42
+ /**
43
+ * Remove only local authentication state.
44
+ */
45
+ clearLocalSession(): void;
46
+ /**
47
+ * Remove the current SDK error without changing the session.
48
+ */
49
+ clearError(): void;
50
+ }
51
+ export interface NexAuthCallbackProps {
52
+ readonly url?: string;
53
+ /**
54
+ * When true, returnTo is opened after callback completion.
55
+ *
56
+ * Defaults to true.
57
+ */
58
+ readonly redirect?: boolean;
59
+ readonly loadingFallback?: ReactNode;
60
+ readonly errorFallback?: ReactNode | ((error: NexAuthError) => ReactNode);
61
+ readonly onSuccess?: (result: AuthorizationCallbackResult) => void | Promise<void>;
62
+ readonly onError?: (error: NexAuthError) => void;
63
+ }
64
+ export interface AuthenticationBoundaryProps {
65
+ readonly children: ReactNode;
66
+ readonly fallback?: ReactNode;
67
+ }
68
+ export interface RequireAuthProps {
69
+ readonly children: ReactNode;
70
+ /**
71
+ * Content rendered while authentication state is loading.
72
+ */
73
+ readonly loadingFallback?: ReactNode;
74
+ /**
75
+ * Content rendered before the browser is redirected for sign-in.
76
+ */
77
+ readonly unauthenticatedFallback?: ReactNode;
78
+ /**
79
+ * Automatically begin login when no session exists.
80
+ *
81
+ * Defaults to true.
82
+ */
83
+ readonly autoLogin?: boolean;
84
+ readonly loginOptions?: LoginOptions;
85
+ readonly onUnauthenticated?: () => void;
86
+ }
87
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@nexauthxyz/react",
3
+ "version": "0.1.2",
4
+ "description": "Official React SDK for NexAuth authentication using OpenID Connect and PKCE.",
5
+ "author": {
6
+ "name": "NexAuth",
7
+ "url": "https://nexauth.xyz"
8
+ },
9
+ "type": "module",
10
+ "main": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "README.md",
21
+ "LICENSE"
22
+ ],
23
+ "sideEffects": false,
24
+ "scripts": {
25
+ "clean": "rimraf dist",
26
+ "build": "tsc -p tsconfig.build.json",
27
+ "typecheck": "tsc --noEmit -p tsconfig.json",
28
+ "test": "vitest run --config vitest.config.ts"
29
+ },
30
+ "peerDependencies": {
31
+ "react": "^18.0.0 || ^19.0.0",
32
+ "react-dom": "^18.0.0 || ^19.0.0"
33
+ },
34
+ "devDependencies": {
35
+ "@testing-library/dom": "^10.4.2",
36
+ "@testing-library/jest-dom": "^7.0.1",
37
+ "@testing-library/react": "^16.3.3",
38
+ "@testing-library/user-event": "^14.6.7",
39
+ "@types/react": "^19.1.12",
40
+ "@types/react-dom": "^19.1.9",
41
+ "jsdom": "^29.1.1",
42
+ "react": "^19.1.1",
43
+ "react-dom": "^19.1.1"
44
+ },
45
+ "engines": {
46
+ "node": ">=20"
47
+ },
48
+ "keywords": [
49
+ "nexauth",
50
+ "react",
51
+ "authentication",
52
+ "authorization",
53
+ "oauth2",
54
+ "oidc",
55
+ "openid-connect",
56
+ "pkce"
57
+ ],
58
+ "license": "MIT",
59
+ "publishConfig": {
60
+ "access": "public"
61
+ },
62
+ "dependencies": {
63
+ "@nexauthxyz/browser": "0.1.2"
64
+ }
65
+ }
package/readme.md ADDED
@@ -0,0 +1,400 @@
1
+ # `@nexauthxyz/react`
2
+
3
+ Official React integration for NexAuth. It provides a context provider, hooks, callback handling, authentication boundaries, protected-content helpers, session-expiration handling, and cross-tab logout synchronization.
4
+
5
+ This package is built on `@nexauthxyz/browser`; it does not replace the browser SDK or duplicate the OIDC protocol implementation.
6
+
7
+ ## Features
8
+
9
+ - `NexAuthProvider` for application-wide authentication state.
10
+ - `useNexAuth()` for session, user, token, login, callback, and logout access.
11
+ - `NexAuthCallback` for authorization callback processing.
12
+ - `Authenticated` and `Unauthenticated` rendering boundaries.
13
+ - `RequireAuth` for protected content and optional automatic login.
14
+ - Existing-session restoration during provider initialization.
15
+ - Automatic local logout when the stored session expires.
16
+ - Session revalidation when a tab becomes active again.
17
+ - Cross-tab logout synchronization using `BroadcastChannel` with a storage-event fallback.
18
+ - Structured NexAuth errors with support request IDs.
19
+
20
+ ## Requirements
21
+
22
+ - React 18 or React 19.
23
+ - React DOM 18 or React DOM 19 for browser applications.
24
+ - A supported version of `@nexauthxyz/browser`.
25
+ - A configured NexAuth application and exact registered redirect URI.
26
+ - An active NexAuth organization subscription/entitlement.
27
+
28
+ ## Installation
29
+
30
+ ```bash
31
+ npm install @nexauthxyz/react @nexauthxyz/browser
32
+ ```
33
+
34
+ `@nexauthxyz/browser` may be installed automatically as a runtime dependency, but explicitly installing compatible versions makes the dependency relationship visible in the application manifest.
35
+
36
+ ## Dependencies and peer dependencies
37
+
38
+ | Package | Relationship | Purpose |
39
+ | ---------------------- | --------------------------- | ---------------------------------------------------------------------------------------------------- |
40
+ | `@nexauthxyz/browser` | Runtime dependency | OIDC discovery, PKCE, token exchange, ID-token validation, UserInfo, storage, and logout. |
41
+ | `react` | Peer dependency | Context, state, effects, callbacks, hooks, and rendering. |
42
+ | `react-dom` | Peer dependency | Browser React rendering in the consuming application. |
43
+ | NexAuth OIDC server | External service dependency | Authorization, tokens, UserInfo, JWKS, logout, application validation, and subscription enforcement. |
44
+
45
+ The React SDK should not bundle its own copy of React. Keeping React as a peer dependency prevents duplicate React runtimes and invalid-hook errors.
46
+
47
+ ## Provider setup
48
+
49
+ Mount one provider near the root of the application:
50
+
51
+ ```tsx
52
+ import { StrictMode } from 'react';
53
+ import { createRoot } from 'react-dom/client';
54
+ import { BrowserRouter } from 'react-router-dom';
55
+ import { NexAuthProvider } from '@nexauthxyz/react';
56
+
57
+ import { App } from './App.js';
58
+
59
+ createRoot(document.getElementById('root')!).render(
60
+ <StrictMode>
61
+ <BrowserRouter>
62
+ <NexAuthProvider
63
+ options={{
64
+ issuer: import.meta.env.VITE_NEXAUTH_ISSUER,
65
+ clientId: import.meta.env.VITE_NEXAUTH_CLIENT_ID,
66
+ redirectUri: `${window.location.origin}/auth/callback`,
67
+ scope: 'openid profile email',
68
+ }}
69
+ onError={(error) => {
70
+ console.error(error.code, error.requestId);
71
+ }}
72
+ >
73
+ <App />
74
+ </NexAuthProvider>
75
+ </BrowserRouter>
76
+ </StrictMode>,
77
+ );
78
+ ```
79
+
80
+ Example Vite environment configuration:
81
+
82
+ ```dotenv
83
+ VITE_NEXAUTH_ISSUER=https://api.nexauth.xyz
84
+ VITE_NEXAUTH_CLIENT_ID=nxa_your_public_client_id
85
+ ```
86
+
87
+ These values are public identifiers, not secrets.
88
+
89
+ ## Provider properties
90
+
91
+ | Property | Required | Default | Description |
92
+ | ---------------- | -------- | -------------------- | ----------------------------------------------------- |
93
+ | `children` | Yes | — | React application subtree. |
94
+ | `options` | Yes | — | Options accepted by `NexAuthBrowserClient`. |
95
+ | `onError` | No | — | Receives normalized SDK errors. |
96
+ | `syncTabs` | No | `true` | Synchronizes logout across tabs. |
97
+ | `tabSyncChannel` | No | Client-specific name | Overrides the authentication synchronization channel. |
98
+
99
+ Treat provider options as immutable. If issuer or client ID must change, remount the provider with a new React key.
100
+
101
+ ## Using the hook
102
+
103
+ ```tsx
104
+ import { useNexAuth } from '@nexauthxyz/react';
105
+
106
+ export function AccountMenu() {
107
+ const {
108
+ status,
109
+ user,
110
+ session,
111
+ accessToken,
112
+ error,
113
+ isLoading,
114
+ isAuthenticated,
115
+ login,
116
+ logout,
117
+ } = useNexAuth();
118
+
119
+ if (isLoading) {
120
+ return <p>Loading authentication…</p>;
121
+ }
122
+
123
+ if (!isAuthenticated) {
124
+ return (
125
+ <button onClick={() => void login({ returnTo: '/dashboard' })}>
126
+ Sign in
127
+ </button>
128
+ );
129
+ }
130
+
131
+ return (
132
+ <section>
133
+ <p>Signed in as {user?.email ?? user?.name ?? user?.sub}</p>
134
+ <p>Status: {status}</p>
135
+ <button onClick={() => void logout()}>Sign out</button>
136
+ {error?.requestId ? <small>Reference: {error.requestId}</small> : null}
137
+ </section>
138
+ );
139
+ }
140
+ ```
141
+
142
+ ## Callback route
143
+
144
+ With React Router:
145
+
146
+ ```tsx
147
+ import { Route, Routes, useNavigate } from 'react-router-dom';
148
+ import { NexAuthCallback } from '@nexauthxyz/react';
149
+
150
+ function AuthCallbackPage() {
151
+ const navigate = useNavigate();
152
+
153
+ return (
154
+ <NexAuthCallback
155
+ loading={<p>Completing secure sign-in…</p>}
156
+ onSuccess={(result) => {
157
+ navigate(result.returnTo ?? '/dashboard', {
158
+ replace: true,
159
+ });
160
+ }}
161
+ onError={(error) => {
162
+ console.error(error.code, error.requestId);
163
+ }}
164
+ />
165
+ );
166
+ }
167
+
168
+ export function AppRoutes() {
169
+ return (
170
+ <Routes>
171
+ <Route path="/auth/callback" element={<AuthCallbackPage />} />
172
+ </Routes>
173
+ );
174
+ }
175
+ ```
176
+
177
+ Register the resulting full callback URI in NexAuth. For local development:
178
+
179
+ ```text
180
+ http://localhost:5173/auth/callback
181
+ ```
182
+
183
+ For production:
184
+
185
+ ```text
186
+ https://shop.example.com/auth/callback
187
+ ```
188
+
189
+ ## Authentication boundaries
190
+
191
+ ```tsx
192
+ import { Authenticated, Unauthenticated } from '@nexauthxyz/react';
193
+
194
+ export function HomePage() {
195
+ return (
196
+ <>
197
+ <Authenticated>
198
+ <p>Private account content</p>
199
+ </Authenticated>
200
+
201
+ <Unauthenticated>
202
+ <p>Sign in to continue.</p>
203
+ </Unauthenticated>
204
+ </>
205
+ );
206
+ }
207
+ ```
208
+
209
+ ## Protected content
210
+
211
+ ```tsx
212
+ import { RequireAuth } from '@nexauthxyz/react';
213
+
214
+ export function DashboardPage() {
215
+ return (
216
+ <RequireAuth
217
+ loading={<p>Checking session…</p>}
218
+ fallback={<p>Authentication is required.</p>}
219
+ loginOptions={{ returnTo: '/dashboard' }}
220
+ >
221
+ <Dashboard />
222
+ </RequireAuth>
223
+ );
224
+ }
225
+ ```
226
+
227
+ Depending on your `RequireAuth` implementation, automatic login can be enabled for unauthenticated users. Avoid repeatedly starting login during an error state; show a retry action instead.
228
+
229
+ ## Calling protected APIs
230
+
231
+ ```tsx
232
+ import { useNexAuth } from '@nexauthxyz/react';
233
+
234
+ export function OrdersButton() {
235
+ const { accessToken } = useNexAuth();
236
+
237
+ const loadOrders = async () => {
238
+ if (!accessToken) {
239
+ return;
240
+ }
241
+
242
+ const response = await fetch('https://api.example.com/orders', {
243
+ headers: {
244
+ Authorization: `Bearer ${accessToken}`,
245
+ },
246
+ });
247
+
248
+ if (!response.ok) {
249
+ throw new Error(`Unable to load orders (${response.status}).`);
250
+ }
251
+ };
252
+
253
+ return <button onClick={() => void loadOrders()}>Load orders</button>;
254
+ }
255
+ ```
256
+
257
+ The protected API must independently validate access tokens. UI boundaries improve user experience but are not a backend authorization control.
258
+
259
+ ## Context API
260
+
261
+ The `useNexAuth()` value includes:
262
+
263
+ | Member | Description |
264
+ | ------------------------ | ------------------------------------------------------------------------- |
265
+ | `status` | `loading`, `authenticated`, `unauthenticated`, or `error`. |
266
+ | `session` | Current validated local session or `null`. |
267
+ | `user` | Current user profile or `null`. |
268
+ | `accessToken` | Current bearer token or `null`. |
269
+ | `error` | Latest normalized `NexAuthError` or `null`. |
270
+ | `isLoading` | Convenience loading flag. |
271
+ | `isAuthenticated` | True only with authenticated status and a current session. |
272
+ | `login(options?)` | Starts Authorization Code + PKCE login. |
273
+ | `handleCallback(input?)` | Completes callback processing. |
274
+ | `logout(options?)` | Clears local state and optionally invokes provider logout. |
275
+ | `refreshSession()` | Reloads the current session from SDK storage. This does not renew tokens. |
276
+ | `clearLocalSession()` | Removes only local authentication state. |
277
+ | `clearError()` | Clears the current React error state. |
278
+
279
+ ## Session lifecycle
280
+
281
+ The provider restores a valid stored session on initialization. It also:
282
+
283
+ - clears expired local sessions;
284
+ - schedules logout for the session expiry time;
285
+ - rechecks the session when the page regains focus or visibility;
286
+ - optionally propagates logout to other tabs.
287
+
288
+ `refreshSession()` means “reload local session state”; it does not call an OAuth refresh-token endpoint.
289
+
290
+ ## Cross-tab synchronization
291
+
292
+ Cross-tab synchronization is enabled by default:
293
+
294
+ ```tsx
295
+ <NexAuthProvider options={options} syncTabs>
296
+ <App />
297
+ </NexAuthProvider>
298
+ ```
299
+
300
+ Disable it when required:
301
+
302
+ ```tsx
303
+ <NexAuthProvider options={options} syncTabs={false}>
304
+ <App />
305
+ </NexAuthProvider>
306
+ ```
307
+
308
+ Only a small logout event is transmitted. Tokens, user profiles, and complete sessions must never be broadcast between tabs.
309
+
310
+ ## Error handling
311
+
312
+ ```tsx
313
+ <NexAuthProvider
314
+ options={options}
315
+ onError={(error) => {
316
+ reportAuthenticationFailure({
317
+ code: error.code,
318
+ requestId: error.requestId,
319
+ });
320
+ }}
321
+ >
322
+ <App />
323
+ </NexAuthProvider>
324
+ ```
325
+
326
+ Avoid sending access tokens, ID tokens, authorization codes, PKCE verifiers, or raw error causes to analytics services. The request ID is designed to correlate a safe user-facing support reference with server audit logs.
327
+
328
+ ## Security and commercial access
329
+
330
+ The npm package can be public. Downloading it does not provide access to paid NexAuth services. The backend must validate:
331
+
332
+ - Client ID and active application status
333
+ - Active organization subscription/entitlement
334
+ - Exact registered redirect URI
335
+ - Allowed browser origin
336
+ - PKCE S256
337
+ - Authorization-code ownership, expiry, and single use
338
+ - User organization/application membership
339
+ - Token audience and requested permissions
340
+
341
+ Never embed client secrets, JWT signing keys, database credentials, or administrative keys in a React application. Browser-delivered values can always be inspected.
342
+
343
+ ## Package relationship
344
+
345
+ ```text
346
+ React application
347
+ └─ @nexauthxyz/react
348
+ ├─ peer: react
349
+ ├─ peer: react-dom
350
+ └─ dependency: @nexauthxyz/browser
351
+ └─ dependency: jose
352
+ ```
353
+
354
+ Publish packages in this order:
355
+
356
+ 1. `@nexauthxyz/browser`
357
+ 2. `@nexauthxyz/react`
358
+
359
+ The browser version referenced by the React package must already exist in the npm registry before publishing React.
360
+
361
+ ## Package validation
362
+
363
+ ```bash
364
+ npm run test --workspace @nexauthxyz/react
365
+ npm run typecheck --workspace @nexauthxyz/react
366
+ npm run build --workspace @nexauthxyz/react
367
+ npm pack --workspace @nexauthxyz/react --dry-run
368
+ ```
369
+
370
+ Verify that the tarball contains only intended public files such as `dist`, `README.md`, and `LICENSE`.
371
+
372
+ ## Troubleshooting
373
+
374
+ ### `useNexAuth` used outside the provider
375
+
376
+ Ensure the component is rendered below `NexAuthProvider`.
377
+
378
+ ### Callback works locally but fails in production
379
+
380
+ Register the production callback separately and ensure it exactly matches `window.location.origin + '/auth/callback'`.
381
+
382
+ ### Vercel or Netlify returns 404 on callback
383
+
384
+ Configure an SPA rewrite so `/auth/callback` serves `index.html` while preserving the query string.
385
+
386
+ ### Login works in one tab but another tab stays authenticated after logout
387
+
388
+ Leave `syncTabs` enabled and verify the browser supports `BroadcastChannel` or storage events.
389
+
390
+ ### Application does not have an active plan
391
+
392
+ This response comes from backend entitlement enforcement. The organization subscription must be active; no frontend SDK setting should bypass it.
393
+
394
+ ### Session expires without renewal
395
+
396
+ This is expected until NexAuth supports a secure refresh-token grant with rotation and replay protection. The React SDK should not fabricate renewal locally.
397
+
398
+ ## License
399
+
400
+ MIT