@skylabs-digital/react-identity-access 3.0.0 → 3.1.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
@@ -8,11 +8,14 @@ A powerful, modern authentication and authorization library for React applicatio
8
8
  - **✨ Magic Link Authentication** - Passwordless authentication via email with automatic verification
9
9
  - **📧 Flexible Login** - Support for both email and phone number authentication
10
10
  - **👥 Role-Based Access Control** - Granular permission system with role hierarchy
11
- - **🛡️ Protected Components** - Easy-to-use components for conditional rendering
12
- - **📱 Multi-Tenant Support** - Built-in support for multi-tenant applications
11
+ - **🛡️ Protected Components & Zone Routing** - Declarative route access control (RFC-005)
12
+ - **📱 Multi-Tenant Support** - Subdomain, selector, and fixed tenant modes
13
13
  - **🎯 TypeScript First** - Full TypeScript support with comprehensive type definitions
14
14
  - **⚡ Modern React** - Built with React hooks and context for optimal performance
15
- - **🔄 Session Management** - Automatic session handling and token refresh
15
+ - **🔄 Session Management** - Automatic refresh, proactive renewal, circuit breaker
16
+ - **🍪 Cross-subdomain Cookie Session** - Optional HttpOnly refresh cookie for seamless SSO between subdomains
17
+ - **🪟 Multi-tab Safety** - Web Locks API coordinates token refresh across browser tabs
18
+ - **🧩 Standalone AuthProvider** - Works with or without AppProvider/TenantProvider
16
19
  - **🎨 Feature Flags** - Built-in feature flag management
17
20
  - **💳 Subscription Management** - Integrated billing and subscription handling
18
21
  - **🎛️ Fully Customizable Components** - All texts, styles, and icons are overridable via props
@@ -29,14 +32,14 @@ yarn add @skylabs-digital/react-identity-access
29
32
 
30
33
  ### 1. Setup Providers
31
34
 
32
- Wrap your application with the required providers in order:
35
+ You can use the library in **two modes**:
36
+
37
+ #### Option A — Multi-tenant setup (recommended for SaaS apps)
38
+
39
+ Wrap your application with the three providers in order:
33
40
 
34
41
  ```tsx
35
- import {
36
- AppProvider,
37
- TenantProvider,
38
- AuthProvider,
39
- } from '@skylabs-digital/react-identity-access';
42
+ import { AppProvider, TenantProvider, AuthProvider } from '@skylabs-digital/react-identity-access';
40
43
 
41
44
  function App() {
42
45
  return (
@@ -48,19 +51,38 @@ function App() {
48
51
  >
49
52
  <TenantProvider
50
53
  config={{
51
- tenantMode: 'selector', // 'subdomain' | 'selector' | 'fixed' | 'optional'
54
+ tenantMode: 'selector', // 'subdomain' | 'selector' | 'fixed'
52
55
  selectorParam: 'tenant',
53
56
  }}
54
57
  >
55
- <AuthProvider>
56
- {/* Your app components */}
57
- </AuthProvider>
58
+ <AuthProvider>{/* Your app components */}</AuthProvider>
58
59
  </TenantProvider>
59
60
  </AppProvider>
60
61
  );
61
62
  }
62
63
  ```
63
64
 
65
+ #### Option B — Standalone AuthProvider (single-tenant or pure auth)
66
+
67
+ `AuthProvider` can run without `AppProvider` and `TenantProvider`. Pass `baseUrl` and `appId` directly via `AuthConfig` — ideal for simple apps that don't need multi-tenancy.
68
+
69
+ ```tsx
70
+ import { AuthProvider } from '@skylabs-digital/react-identity-access';
71
+
72
+ function App() {
73
+ return (
74
+ <AuthProvider
75
+ config={{
76
+ baseUrl: 'https://your-api.com',
77
+ appId: 'your-app-id',
78
+ }}
79
+ >
80
+ {/* Your app components */}
81
+ </AuthProvider>
82
+ );
83
+ }
84
+ ```
85
+
64
86
  ### 2. Traditional Authentication
65
87
 
66
88
  ```tsx
@@ -151,64 +173,64 @@ Traditional email/phone + password login form.
151
173
 
152
174
  #### Props (`LoginFormProps`)
153
175
 
154
- | Prop | Type | Default | Description |
155
- |------|------|---------|-------------|
156
- | `copy` | `LoginFormCopy` | See below | Override user-facing texts |
157
- | `styles` | `LoginFormStyles` | See below | Override inline styles |
158
- | `icons` | `LoginFormIcons` | Eye/EyeOff SVGs | Override password toggle icons |
159
- | `onSuccess` | `(data: any) => void` | — | Called after successful login |
160
- | `onError` | `(error: string) => void` | — | Called on login failure |
161
- | `onForgotPassword` | `() => void` | — | Navigate to forgot password |
162
- | `onSignupClick` | `() => void` | — | Navigate to signup |
163
- | `onMagicLinkClick` | `() => void` | — | Navigate to magic link |
164
- | `showForgotPassword` | `boolean` | `true` | Show "Forgot password?" link |
165
- | `showSignupLink` | `boolean` | `true` | Show signup link |
166
- | `showMagicLinkOption` | `boolean` | `false` | Show magic link option |
167
- | `className` | `string` | — | CSS class for the root element |
176
+ | Prop | Type | Default | Description |
177
+ | --------------------- | ------------------------- | --------------- | ------------------------------ |
178
+ | `copy` | `LoginFormCopy` | See below | Override user-facing texts |
179
+ | `styles` | `LoginFormStyles` | See below | Override inline styles |
180
+ | `icons` | `LoginFormIcons` | Eye/EyeOff SVGs | Override password toggle icons |
181
+ | `onSuccess` | `(data: any) => void` | — | Called after successful login |
182
+ | `onError` | `(error: string) => void` | — | Called on login failure |
183
+ | `onForgotPassword` | `() => void` | — | Navigate to forgot password |
184
+ | `onSignupClick` | `() => void` | — | Navigate to signup |
185
+ | `onMagicLinkClick` | `() => void` | — | Navigate to magic link |
186
+ | `showForgotPassword` | `boolean` | `true` | Show "Forgot password?" link |
187
+ | `showSignupLink` | `boolean` | `true` | Show signup link |
188
+ | `showMagicLinkOption` | `boolean` | `false` | Show magic link option |
189
+ | `className` | `string` | — | CSS class for the root element |
168
190
 
169
191
  #### Copy (`LoginFormCopy`)
170
192
 
171
- | Key | Default |
172
- |-----|---------|
173
- | `title` | `'Sign In'` |
174
- | `usernameLabel` | `'Email or Phone'` |
175
- | `usernamePlaceholder` | `'Enter your email or phone number'` |
176
- | `passwordLabel` | `'Password'` |
177
- | `passwordPlaceholder` | `'Enter your password'` |
178
- | `submitButton` | `'Sign In'` |
179
- | `loadingText` | `'Signing in...'` |
180
- | `errorMessage` | `'Invalid credentials'` |
181
- | `forgotPasswordLink` | `'Forgot your password?'` |
182
- | `signupLink` | `'Sign up here'` |
183
- | `signupText` | `"Don't have an account?"` |
184
- | `magicLinkText` | `'Prefer passwordless?'` |
185
- | `magicLinkLink` | `'Use Magic Link'` |
186
- | `tenantNotFoundError` | `'Tenant not found'` |
187
- | `dividerBullet` | `'•'` |
188
- | `showPasswordAriaLabel` | `'Show password'` |
189
- | `hidePasswordAriaLabel` | `'Hide password'` |
193
+ | Key | Default |
194
+ | ----------------------- | ------------------------------------ |
195
+ | `title` | `'Sign In'` |
196
+ | `usernameLabel` | `'Email or Phone'` |
197
+ | `usernamePlaceholder` | `'Enter your email or phone number'` |
198
+ | `passwordLabel` | `'Password'` |
199
+ | `passwordPlaceholder` | `'Enter your password'` |
200
+ | `submitButton` | `'Sign In'` |
201
+ | `loadingText` | `'Signing in...'` |
202
+ | `errorMessage` | `'Invalid credentials'` |
203
+ | `forgotPasswordLink` | `'Forgot your password?'` |
204
+ | `signupLink` | `'Sign up here'` |
205
+ | `signupText` | `"Don't have an account?"` |
206
+ | `magicLinkText` | `'Prefer passwordless?'` |
207
+ | `magicLinkLink` | `'Use Magic Link'` |
208
+ | `tenantNotFoundError` | `'Tenant not found'` |
209
+ | `dividerBullet` | `'•'` |
210
+ | `showPasswordAriaLabel` | `'Show password'` |
211
+ | `hidePasswordAriaLabel` | `'Hide password'` |
190
212
 
191
213
  #### Styles (`LoginFormStyles`)
192
214
 
193
- | Key | Targets |
194
- |-----|---------|
195
- | `container` | Root wrapper |
196
- | `title` | `<h2>` heading |
197
- | `form` | `<form>` element |
198
- | `fieldGroup` | Each label+input group |
199
- | `label` | `<label>` elements |
200
- | `input` | `<input>` elements |
201
- | `inputError` | Input in error state (merged on top of `input`) |
202
- | `inputContainer` | Password field wrapper |
203
- | `inputWithIcon` | Password input with toggle icon |
204
- | `passwordToggle` | Show/hide password button |
205
- | `button` | Submit button |
206
- | `buttonDisabled` | Disabled state (merged on top of `button`) |
207
- | `buttonLoading` | Loading state (merged on top of `button`) |
208
- | `errorText` | Error message text |
209
- | `linkContainer` | Links section wrapper |
210
- | `link` | `<a>` link elements |
211
- | `divider` | Bullet divider between links |
215
+ | Key | Targets |
216
+ | ---------------- | ----------------------------------------------- |
217
+ | `container` | Root wrapper |
218
+ | `title` | `<h2>` heading |
219
+ | `form` | `<form>` element |
220
+ | `fieldGroup` | Each label+input group |
221
+ | `label` | `<label>` elements |
222
+ | `input` | `<input>` elements |
223
+ | `inputError` | Input in error state (merged on top of `input`) |
224
+ | `inputContainer` | Password field wrapper |
225
+ | `inputWithIcon` | Password input with toggle icon |
226
+ | `passwordToggle` | Show/hide password button |
227
+ | `button` | Submit button |
228
+ | `buttonDisabled` | Disabled state (merged on top of `button`) |
229
+ | `buttonLoading` | Loading state (merged on top of `button`) |
230
+ | `errorText` | Error message text |
231
+ | `linkContainer` | Links section wrapper |
232
+ | `link` | `<a>` link elements |
233
+ | `divider` | Bullet divider between links |
212
234
 
213
235
  #### Usage Example
214
236
 
@@ -228,7 +250,7 @@ Traditional email/phone + password login form.
228
250
  showPassword: <MyEyeIcon />,
229
251
  hidePassword: <MyEyeOffIcon />,
230
252
  }}
231
- onSuccess={(data) => navigate('/dashboard')}
253
+ onSuccess={data => navigate('/dashboard')}
232
254
  onMagicLinkClick={() => navigate('/magic-link')}
233
255
  showMagicLinkOption
234
256
  />
@@ -242,74 +264,74 @@ User registration form with email/phone, password, and optional tenant creation.
242
264
 
243
265
  #### Props (`SignupFormProps`)
244
266
 
245
- | Prop | Type | Default | Description |
246
- |------|------|---------|-------------|
247
- | `copy` | `SignupFormCopy` | See below | Override user-facing texts |
248
- | `styles` | `SignupFormStyles` | See below | Override inline styles |
249
- | `signupType` | `'user' \| 'tenant'` | `'user'` | User signup or tenant admin signup |
250
- | `onSuccess` | `(data: any) => void` | — | Called after successful signup |
251
- | `onError` | `(error: string) => void` | — | Called on signup failure |
252
- | `onLoginClick` | `() => void` | — | Navigate to login |
253
- | `onMagicLinkClick` | `() => void` | — | Navigate to magic link |
254
- | `showLoginLink` | `boolean` | `true` | Show login link |
255
- | `showMagicLinkOption` | `boolean` | `false` | Show magic link option |
256
- | `className` | `string` | — | CSS class for the root element |
267
+ | Prop | Type | Default | Description |
268
+ | --------------------- | ------------------------- | --------- | ---------------------------------- |
269
+ | `copy` | `SignupFormCopy` | See below | Override user-facing texts |
270
+ | `styles` | `SignupFormStyles` | See below | Override inline styles |
271
+ | `signupType` | `'user' \| 'tenant'` | `'user'` | User signup or tenant admin signup |
272
+ | `onSuccess` | `(data: any) => void` | — | Called after successful signup |
273
+ | `onError` | `(error: string) => void` | — | Called on signup failure |
274
+ | `onLoginClick` | `() => void` | — | Navigate to login |
275
+ | `onMagicLinkClick` | `() => void` | — | Navigate to magic link |
276
+ | `showLoginLink` | `boolean` | `true` | Show login link |
277
+ | `showMagicLinkOption` | `boolean` | `false` | Show magic link option |
278
+ | `className` | `string` | — | CSS class for the root element |
257
279
 
258
280
  #### Copy (`SignupFormCopy`)
259
281
 
260
- | Key | Default |
261
- |-----|---------|
262
- | `title` | `'Create Account'` |
263
- | `nameLabel` | `'First Name'` |
264
- | `namePlaceholder` | `'Enter your first name'` |
265
- | `lastNameLabel` | `'Last Name'` |
266
- | `lastNamePlaceholder` | `'Enter your last name'` |
267
- | `emailLabel` | `'Email'` |
268
- | `emailPlaceholder` | `'Enter your email'` |
269
- | `phoneNumberLabel` | `'Phone Number'` |
270
- | `phoneNumberPlaceholder` | `'Enter your phone number'` |
271
- | `passwordLabel` | `'Password'` |
272
- | `passwordPlaceholder` | `'Enter your password'` |
273
- | `confirmPasswordLabel` | `'Confirm Password'` |
274
- | `confirmPasswordPlaceholder` | `'Confirm your password'` |
275
- | `tenantNameLabel` | `'Organization Name'` |
276
- | `tenantNamePlaceholder` | `'Enter your organization name'` |
277
- | `submitButton` | `'Create Account'` |
278
- | `loadingText` | `'Creating account...'` |
279
- | `errorMessage` | `'Failed to create account'` |
280
- | `passwordMismatchError` | `'Passwords do not match'` |
281
- | `loginLink` | `'Sign in here'` |
282
- | `loginText` | `'Already have an account?'` |
283
- | `magicLinkText` | `'Prefer passwordless?'` |
284
- | `magicLinkLink` | `'Use Magic Link'` |
285
- | `isAdminLabel` | `'Create new organization'` |
286
- | `isAdminDescription` | `'Check this if you want to create a new organization'` |
287
- | `contactMethodHint` | `'At least one contact method (email or phone) is required'` |
288
- | `tenantNotFoundError` | `'Tenant not found'` |
289
- | `dividerBullet` | `'•'` |
282
+ | Key | Default |
283
+ | ---------------------------- | ------------------------------------------------------------ |
284
+ | `title` | `'Create Account'` |
285
+ | `nameLabel` | `'First Name'` |
286
+ | `namePlaceholder` | `'Enter your first name'` |
287
+ | `lastNameLabel` | `'Last Name'` |
288
+ | `lastNamePlaceholder` | `'Enter your last name'` |
289
+ | `emailLabel` | `'Email'` |
290
+ | `emailPlaceholder` | `'Enter your email'` |
291
+ | `phoneNumberLabel` | `'Phone Number'` |
292
+ | `phoneNumberPlaceholder` | `'Enter your phone number'` |
293
+ | `passwordLabel` | `'Password'` |
294
+ | `passwordPlaceholder` | `'Enter your password'` |
295
+ | `confirmPasswordLabel` | `'Confirm Password'` |
296
+ | `confirmPasswordPlaceholder` | `'Confirm your password'` |
297
+ | `tenantNameLabel` | `'Organization Name'` |
298
+ | `tenantNamePlaceholder` | `'Enter your organization name'` |
299
+ | `submitButton` | `'Create Account'` |
300
+ | `loadingText` | `'Creating account...'` |
301
+ | `errorMessage` | `'Failed to create account'` |
302
+ | `passwordMismatchError` | `'Passwords do not match'` |
303
+ | `loginLink` | `'Sign in here'` |
304
+ | `loginText` | `'Already have an account?'` |
305
+ | `magicLinkText` | `'Prefer passwordless?'` |
306
+ | `magicLinkLink` | `'Use Magic Link'` |
307
+ | `isAdminLabel` | `'Create new organization'` |
308
+ | `isAdminDescription` | `'Check this if you want to create a new organization'` |
309
+ | `contactMethodHint` | `'At least one contact method (email or phone) is required'` |
310
+ | `tenantNotFoundError` | `'Tenant not found'` |
311
+ | `dividerBullet` | `'•'` |
290
312
 
291
313
  #### Styles (`SignupFormStyles`)
292
314
 
293
- | Key | Targets |
294
- |-----|---------|
295
- | `container` | Root wrapper |
296
- | `title` | `<h2>` heading |
297
- | `form` | `<form>` element |
298
- | `fieldGroup` | Each label+input group |
299
- | `label` | `<label>` elements |
300
- | `input` | `<input>` elements |
301
- | `inputError` | Input in error state |
302
- | `checkbox` | Checkbox input |
303
- | `checkboxContainer` | Checkbox + label wrapper |
304
- | `checkboxLabel` | Checkbox label text |
305
- | `button` | Submit button |
306
- | `buttonDisabled` | Disabled state (merged on top of `button`) |
307
- | `buttonLoading` | Loading state (merged on top of `button`) |
308
- | `errorText` | Error message text |
309
- | `linkContainer` | Links section wrapper |
310
- | `link` | `<a>` link elements |
311
- | `divider` | Bullet divider between links |
312
- | `hintText` | Contact method hint text |
315
+ | Key | Targets |
316
+ | ------------------- | ------------------------------------------ |
317
+ | `container` | Root wrapper |
318
+ | `title` | `<h2>` heading |
319
+ | `form` | `<form>` element |
320
+ | `fieldGroup` | Each label+input group |
321
+ | `label` | `<label>` elements |
322
+ | `input` | `<input>` elements |
323
+ | `inputError` | Input in error state |
324
+ | `checkbox` | Checkbox input |
325
+ | `checkboxContainer` | Checkbox + label wrapper |
326
+ | `checkboxLabel` | Checkbox label text |
327
+ | `button` | Submit button |
328
+ | `buttonDisabled` | Disabled state (merged on top of `button`) |
329
+ | `buttonLoading` | Loading state (merged on top of `button`) |
330
+ | `errorText` | Error message text |
331
+ | `linkContainer` | Links section wrapper |
332
+ | `link` | `<a>` link elements |
333
+ | `divider` | Bullet divider between links |
334
+ | `hintText` | Contact method hint text |
313
335
 
314
336
  ---
315
337
 
@@ -319,71 +341,71 @@ Passwordless Magic Link send form. Handles both new and existing users.
319
341
 
320
342
  #### Props (`MagicLinkFormProps`)
321
343
 
322
- | Prop | Type | Default | Description |
323
- |------|------|---------|-------------|
324
- | `copy` | `MagicLinkFormCopy` | See below | Override user-facing texts |
325
- | `styles` | `MagicLinkFormStyles` | See below | Override inline styles |
326
- | `onSuccess` | `(data: any) => void` | — | Called after magic link sent |
327
- | `onError` | `(error: string) => void` | — | Called on failure |
328
- | `onLoginClick` | `() => void` | — | Navigate to login |
329
- | `onSignupClick` | `() => void` | — | Navigate to signup |
330
- | `showTraditionalLinks` | `boolean` | `true` | Show login/signup links |
331
- | `className` | `string` | — | CSS class for the root element |
332
- | `verifyToken` | `string` | — | Auto-verify a magic link token |
333
- | `frontendUrl` | `string` | `window.location.origin` | Base URL for the magic link callback |
344
+ | Prop | Type | Default | Description |
345
+ | ---------------------- | ------------------------- | ------------------------ | ------------------------------------ |
346
+ | `copy` | `MagicLinkFormCopy` | See below | Override user-facing texts |
347
+ | `styles` | `MagicLinkFormStyles` | See below | Override inline styles |
348
+ | `onSuccess` | `(data: any) => void` | — | Called after magic link sent |
349
+ | `onError` | `(error: string) => void` | — | Called on failure |
350
+ | `onLoginClick` | `() => void` | — | Navigate to login |
351
+ | `onSignupClick` | `() => void` | — | Navigate to signup |
352
+ | `showTraditionalLinks` | `boolean` | `true` | Show login/signup links |
353
+ | `className` | `string` | — | CSS class for the root element |
354
+ | `verifyToken` | `string` | — | Auto-verify a magic link token |
355
+ | `frontendUrl` | `string` | `window.location.origin` | Base URL for the magic link callback |
334
356
 
335
357
  #### Copy (`MagicLinkFormCopy`)
336
358
 
337
- | Key | Default |
338
- |-----|---------|
339
- | `title` | `'Sign In with Magic Link'` |
340
- | `description` | `"Enter your email to receive a magic link..."` |
341
- | `emailLabel` | `'Email'` |
342
- | `emailPlaceholder` | `'Enter your email'` |
343
- | `nameLabel` | `'Name'` |
344
- | `namePlaceholder` | `'Enter your name'` |
345
- | `lastNameLabel` | `'Last Name'` |
346
- | `lastNamePlaceholder` | `'Enter your last name'` |
347
- | `submitButton` | `'Send Magic Link'` |
348
- | `loadingText` | `'Sending magic link...'` |
349
- | `successMessage` | `'Magic link sent! Check your email...'` |
350
- | `errorMessage` | `'Failed to send magic link. Please try again.'` |
351
- | `verifyingText` | `'Verifying magic link...'` |
352
- | `verifyingDescription` | `'Please wait while we verify your magic link...'` |
353
- | `showNameToggle` | `'New user? Add your name'` |
354
- | `hideNameToggle` | `'Existing user? Hide name fields'` |
355
- | `loginLink` | `'Sign in with password'` |
356
- | `loginText` | `'Already have an account?'` |
357
- | `signupLink` | `'Sign up with password'` |
358
- | `signupText` | `'Prefer traditional signup?'` |
359
- | `tenantNotFoundError` | `'Tenant not found'` |
360
- | `missingTenantOrEmailError` | `'Missing tenant or email'` |
361
- | `dividerBullet` | `'•'` |
359
+ | Key | Default |
360
+ | --------------------------- | -------------------------------------------------- |
361
+ | `title` | `'Sign In with Magic Link'` |
362
+ | `description` | `"Enter your email to receive a magic link..."` |
363
+ | `emailLabel` | `'Email'` |
364
+ | `emailPlaceholder` | `'Enter your email'` |
365
+ | `nameLabel` | `'Name'` |
366
+ | `namePlaceholder` | `'Enter your name'` |
367
+ | `lastNameLabel` | `'Last Name'` |
368
+ | `lastNamePlaceholder` | `'Enter your last name'` |
369
+ | `submitButton` | `'Send Magic Link'` |
370
+ | `loadingText` | `'Sending magic link...'` |
371
+ | `successMessage` | `'Magic link sent! Check your email...'` |
372
+ | `errorMessage` | `'Failed to send magic link. Please try again.'` |
373
+ | `verifyingText` | `'Verifying magic link...'` |
374
+ | `verifyingDescription` | `'Please wait while we verify your magic link...'` |
375
+ | `showNameToggle` | `'New user? Add your name'` |
376
+ | `hideNameToggle` | `'Existing user? Hide name fields'` |
377
+ | `loginLink` | `'Sign in with password'` |
378
+ | `loginText` | `'Already have an account?'` |
379
+ | `signupLink` | `'Sign up with password'` |
380
+ | `signupText` | `'Prefer traditional signup?'` |
381
+ | `tenantNotFoundError` | `'Tenant not found'` |
382
+ | `missingTenantOrEmailError` | `'Missing tenant or email'` |
383
+ | `dividerBullet` | `'•'` |
362
384
 
363
385
  #### Styles (`MagicLinkFormStyles`)
364
386
 
365
- | Key | Targets |
366
- |-----|---------|
367
- | `container` | Root wrapper |
368
- | `title` | `<h2>` heading |
369
- | `description` | Description paragraph |
370
- | `form` | `<form>` element |
371
- | `fieldGroup` | Each label+input group |
372
- | `label` | `<label>` elements |
373
- | `input` | `<input>` elements |
374
- | `inputError` | Input in error state |
375
- | `button` | Submit button |
376
- | `buttonDisabled` | Disabled state (merged on top of `button`) |
377
- | `buttonLoading` | Loading state (merged on top of `button`) |
378
- | `errorText` | Error message text |
379
- | `successText` | Success message text |
380
- | `linkContainer` | Links section wrapper |
381
- | `link` | `<a>` link elements |
382
- | `divider` | Bullet divider between links |
383
- | `verifyingContainer` | Verification loading wrapper |
384
- | `verifyingText` | Verification description text |
385
- | `toggleContainer` | Name fields toggle wrapper |
386
- | `toggleLink` | "New user? Add your name" toggle button |
387
+ | Key | Targets |
388
+ | -------------------- | ------------------------------------------ |
389
+ | `container` | Root wrapper |
390
+ | `title` | `<h2>` heading |
391
+ | `description` | Description paragraph |
392
+ | `form` | `<form>` element |
393
+ | `fieldGroup` | Each label+input group |
394
+ | `label` | `<label>` elements |
395
+ | `input` | `<input>` elements |
396
+ | `inputError` | Input in error state |
397
+ | `button` | Submit button |
398
+ | `buttonDisabled` | Disabled state (merged on top of `button`) |
399
+ | `buttonLoading` | Loading state (merged on top of `button`) |
400
+ | `errorText` | Error message text |
401
+ | `successText` | Success message text |
402
+ | `linkContainer` | Links section wrapper |
403
+ | `link` | `<a>` link elements |
404
+ | `divider` | Bullet divider between links |
405
+ | `verifyingContainer` | Verification loading wrapper |
406
+ | `verifyingText` | Verification description text |
407
+ | `toggleContainer` | Name fields toggle wrapper |
408
+ | `toggleLink` | "New user? Add your name" toggle button |
387
409
 
388
410
  ---
389
411
 
@@ -393,59 +415,59 @@ Automatic Magic Link verification component. Reads token from URL params or acce
393
415
 
394
416
  #### Props (`MagicLinkVerifyProps`)
395
417
 
396
- | Prop | Type | Default | Description |
397
- |------|------|---------|-------------|
398
- | `copy` | `MagicLinkVerifyCopy` | See below | Override user-facing texts |
399
- | `styles` | `MagicLinkVerifyStyles` | See below | Override inline styles |
400
- | `icons` | `MagicLinkVerifyIcons` | SVG icons | Override loading/success/error icons |
401
- | `onSuccess` | `(data: any) => void` | — | Called after successful verification |
402
- | `onError` | `(error: string) => void` | — | Called on verification failure |
403
- | `onRetry` | `() => void` | — | Called before retry attempt |
404
- | `onBackToLogin` | `() => void` | — | Navigate back to login |
405
- | `className` | `string` | — | CSS class for the root element |
406
- | `token` | `string` | URL param | Magic link token (auto-extracted from `?token=`) |
407
- | `email` | `string` | URL param | User email (auto-extracted from `?email=`) |
408
- | `appId` | `string` | URL param | App ID (auto-extracted from `?appId=`) |
409
- | `tenantSlug` | `string` | URL param | Tenant slug (auto-extracted from `?tenantSlug=`) |
410
- | `autoRedirectDelay` | `number` | `3000` | Milliseconds before auto-redirect (0 to disable) |
418
+ | Prop | Type | Default | Description |
419
+ | ------------------- | ------------------------- | --------- | ------------------------------------------------ |
420
+ | `copy` | `MagicLinkVerifyCopy` | See below | Override user-facing texts |
421
+ | `styles` | `MagicLinkVerifyStyles` | See below | Override inline styles |
422
+ | `icons` | `MagicLinkVerifyIcons` | SVG icons | Override loading/success/error icons |
423
+ | `onSuccess` | `(data: any) => void` | — | Called after successful verification |
424
+ | `onError` | `(error: string) => void` | — | Called on verification failure |
425
+ | `onRetry` | `() => void` | — | Called before retry attempt |
426
+ | `onBackToLogin` | `() => void` | — | Navigate back to login |
427
+ | `className` | `string` | — | CSS class for the root element |
428
+ | `token` | `string` | URL param | Magic link token (auto-extracted from `?token=`) |
429
+ | `email` | `string` | URL param | User email (auto-extracted from `?email=`) |
430
+ | `appId` | `string` | URL param | App ID (auto-extracted from `?appId=`) |
431
+ | `tenantSlug` | `string` | URL param | Tenant slug (auto-extracted from `?tenantSlug=`) |
432
+ | `autoRedirectDelay` | `number` | `3000` | Milliseconds before auto-redirect (0 to disable) |
411
433
 
412
434
  #### Copy (`MagicLinkVerifyCopy`)
413
435
 
414
- | Key | Default |
415
- |-----|---------|
416
- | `title` | `'Verifying Magic Link'` |
417
- | `verifyingMessage` | `'Please wait while we verify your magic link...'` |
418
- | `successMessage` | `'Magic link verified successfully! You are now logged in.'` |
419
- | `errorMessage` | `'Failed to verify magic link. The link may be expired or invalid.'` |
420
- | `redirectingMessage` | `'Redirecting you to the dashboard...'` |
421
- | `retryButton` | `'Try Again'` |
422
- | `backToLoginButton` | `'Back to Login'` |
423
- | `missingParamsError` | `'Missing required parameters: token or email'` |
436
+ | Key | Default |
437
+ | -------------------- | -------------------------------------------------------------------- |
438
+ | `title` | `'Verifying Magic Link'` |
439
+ | `verifyingMessage` | `'Please wait while we verify your magic link...'` |
440
+ | `successMessage` | `'Magic link verified successfully! You are now logged in.'` |
441
+ | `errorMessage` | `'Failed to verify magic link. The link may be expired or invalid.'` |
442
+ | `redirectingMessage` | `'Redirecting you to the dashboard...'` |
443
+ | `retryButton` | `'Try Again'` |
444
+ | `backToLoginButton` | `'Back to Login'` |
445
+ | `missingParamsError` | `'Missing required parameters: token or email'` |
424
446
 
425
447
  #### Styles (`MagicLinkVerifyStyles`)
426
448
 
427
- | Key | Targets |
428
- |-----|---------|
429
- | `container` | Root wrapper |
430
- | `card` | Inner card (kept for compatibility) |
431
- | `title` | `<h1>` heading |
432
- | `message` | Verifying/redirecting message |
433
- | `successMessage` | Success state message |
434
- | `errorMessage` | Error state message |
435
- | `spinner` | Loading spinner |
436
- | `buttonContainer` | Error buttons wrapper |
437
- | `retryButton` | "Try Again" button |
438
- | `retryButtonHover` | Hover state for retry button |
439
- | `backButton` | "Back to Login" button |
440
- | `backButtonHover` | Hover state for back button |
449
+ | Key | Targets |
450
+ | ------------------ | ----------------------------------- |
451
+ | `container` | Root wrapper |
452
+ | `card` | Inner card (kept for compatibility) |
453
+ | `title` | `<h1>` heading |
454
+ | `message` | Verifying/redirecting message |
455
+ | `successMessage` | Success state message |
456
+ | `errorMessage` | Error state message |
457
+ | `spinner` | Loading spinner |
458
+ | `buttonContainer` | Error buttons wrapper |
459
+ | `retryButton` | "Try Again" button |
460
+ | `retryButtonHover` | Hover state for retry button |
461
+ | `backButton` | "Back to Login" button |
462
+ | `backButtonHover` | Hover state for back button |
441
463
 
442
464
  #### Icons (`MagicLinkVerifyIcons`)
443
465
 
444
- | Key | Default | Description |
445
- |-----|---------|-------------|
446
- | `loading` | Animated spinner | Shown during verification |
447
- | `success` | Green checkmark SVG | Shown on success |
448
- | `error` | Red X circle SVG | Shown on error |
466
+ | Key | Default | Description |
467
+ | --------- | ------------------- | ------------------------- |
468
+ | `loading` | Animated spinner | Shown during verification |
469
+ | `success` | Green checkmark SVG | Shown on success |
470
+ | `error` | Red X circle SVG | Shown on error |
449
471
 
450
472
  ---
451
473
 
@@ -455,68 +477,68 @@ Password reset flow with two modes: request (send email) and reset (set new pass
455
477
 
456
478
  #### Props (`PasswordRecoveryFormProps`)
457
479
 
458
- | Prop | Type | Default | Description |
459
- |------|------|---------|-------------|
460
- | `copy` | `PasswordRecoveryFormCopy` | See below | Override user-facing texts |
461
- | `styles` | `PasswordRecoveryFormStyles` | See below | Override inline styles |
462
- | `mode` | `'request' \| 'reset'` | `'request'` | Current form mode |
463
- | `token` | `string` | — | Pre-fill reset token |
464
- | `onSuccess` | `(data?: any) => void` | — | Called after success |
465
- | `onError` | `(error: string) => void` | — | Called on failure |
466
- | `onBackToLogin` | `() => void` | — | Navigate back to login |
467
- | `onModeChange` | `(mode: 'request' \| 'reset') => void` | — | Show mode switch links |
468
- | `className` | `string` | — | CSS class for the root element |
480
+ | Prop | Type | Default | Description |
481
+ | --------------- | -------------------------------------- | ----------- | ------------------------------ |
482
+ | `copy` | `PasswordRecoveryFormCopy` | See below | Override user-facing texts |
483
+ | `styles` | `PasswordRecoveryFormStyles` | See below | Override inline styles |
484
+ | `mode` | `'request' \| 'reset'` | `'request'` | Current form mode |
485
+ | `token` | `string` | — | Pre-fill reset token |
486
+ | `onSuccess` | `(data?: any) => void` | — | Called after success |
487
+ | `onError` | `(error: string) => void` | — | Called on failure |
488
+ | `onBackToLogin` | `() => void` | — | Navigate back to login |
489
+ | `onModeChange` | `(mode: 'request' \| 'reset') => void` | — | Show mode switch links |
490
+ | `className` | `string` | — | CSS class for the root element |
469
491
 
470
492
  #### Copy (`PasswordRecoveryFormCopy`)
471
493
 
472
- | Key | Default |
473
- |-----|---------|
474
- | `title` | `'Reset Password'` |
475
- | `subtitle` | `"Enter your email address and we'll send you a link..."` |
476
- | `emailLabel` | `'Email'` |
477
- | `emailPlaceholder` | `'Enter your email'` |
478
- | `submitButton` | `'Send Reset Link'` |
479
- | `loadingText` | `'Sending...'` |
480
- | `successMessage` | `'Password reset link sent! Check your email.'` |
481
- | `errorMessage` | `'Failed to send reset link'` |
482
- | `backToLoginLink` | `'Back to Sign In'` |
483
- | `resetTitle` | `'Set New Password'` |
484
- | `resetSubtitle` | `'Enter your reset token and new password.'` |
485
- | `tokenLabel` | `'Reset Token'` |
486
- | `tokenPlaceholder` | `'Enter reset token from email'` |
487
- | `newPasswordLabel` | `'New Password'` |
488
- | `newPasswordPlaceholder` | `'Enter new password'` |
489
- | `confirmPasswordLabel` | `'Confirm Password'` |
490
- | `confirmPasswordPlaceholder` | `'Confirm new password'` |
491
- | `resetSubmitButton` | `'Reset Password'` |
492
- | `resetLoadingText` | `'Resetting...'` |
493
- | `resetSuccessMessage` | `'Password reset successfully!'` |
494
- | `passwordMismatchError` | `'Passwords do not match'` |
495
- | `requestNewLinkLink` | `'Request New Link'` |
496
- | `haveTokenLink` | `'I have a token'` |
497
- | `tenantNotFoundError` | `'Tenant not found'` |
498
- | `dividerBullet` | `'•'` |
494
+ | Key | Default |
495
+ | ---------------------------- | --------------------------------------------------------- |
496
+ | `title` | `'Reset Password'` |
497
+ | `subtitle` | `"Enter your email address and we'll send you a link..."` |
498
+ | `emailLabel` | `'Email'` |
499
+ | `emailPlaceholder` | `'Enter your email'` |
500
+ | `submitButton` | `'Send Reset Link'` |
501
+ | `loadingText` | `'Sending...'` |
502
+ | `successMessage` | `'Password reset link sent! Check your email.'` |
503
+ | `errorMessage` | `'Failed to send reset link'` |
504
+ | `backToLoginLink` | `'Back to Sign In'` |
505
+ | `resetTitle` | `'Set New Password'` |
506
+ | `resetSubtitle` | `'Enter your reset token and new password.'` |
507
+ | `tokenLabel` | `'Reset Token'` |
508
+ | `tokenPlaceholder` | `'Enter reset token from email'` |
509
+ | `newPasswordLabel` | `'New Password'` |
510
+ | `newPasswordPlaceholder` | `'Enter new password'` |
511
+ | `confirmPasswordLabel` | `'Confirm Password'` |
512
+ | `confirmPasswordPlaceholder` | `'Confirm new password'` |
513
+ | `resetSubmitButton` | `'Reset Password'` |
514
+ | `resetLoadingText` | `'Resetting...'` |
515
+ | `resetSuccessMessage` | `'Password reset successfully!'` |
516
+ | `passwordMismatchError` | `'Passwords do not match'` |
517
+ | `requestNewLinkLink` | `'Request New Link'` |
518
+ | `haveTokenLink` | `'I have a token'` |
519
+ | `tenantNotFoundError` | `'Tenant not found'` |
520
+ | `dividerBullet` | `'•'` |
499
521
 
500
522
  #### Styles (`PasswordRecoveryFormStyles`)
501
523
 
502
- | Key | Targets |
503
- |-----|---------|
504
- | `container` | Root wrapper |
505
- | `title` | `<h2>` heading |
506
- | `subtitle` | Subtitle paragraph |
507
- | `form` | `<form>` element |
508
- | `fieldGroup` | Each label+input group |
509
- | `label` | `<label>` elements |
510
- | `input` | `<input>` elements |
511
- | `inputError` | Input in error state |
512
- | `button` | Submit button |
513
- | `buttonDisabled` | Disabled state (merged on top of `button`) |
514
- | `buttonLoading` | Loading state (merged on top of `button`) |
515
- | `errorText` | Error message text |
516
- | `successText` | Success message text |
517
- | `linkContainer` | Links section wrapper |
518
- | `link` | `<a>` link elements |
519
- | `modeSwitchDivider` | Bullet divider between mode links |
524
+ | Key | Targets |
525
+ | ------------------- | ------------------------------------------ |
526
+ | `container` | Root wrapper |
527
+ | `title` | `<h2>` heading |
528
+ | `subtitle` | Subtitle paragraph |
529
+ | `form` | `<form>` element |
530
+ | `fieldGroup` | Each label+input group |
531
+ | `label` | `<label>` elements |
532
+ | `input` | `<input>` elements |
533
+ | `inputError` | Input in error state |
534
+ | `button` | Submit button |
535
+ | `buttonDisabled` | Disabled state (merged on top of `button`) |
536
+ | `buttonLoading` | Loading state (merged on top of `button`) |
537
+ | `errorText` | Error message text |
538
+ | `successText` | Success message text |
539
+ | `linkContainer` | Links section wrapper |
540
+ | `link` | `<a>` link elements |
541
+ | `modeSwitchDivider` | Bullet divider between mode links |
520
542
 
521
543
  ---
522
544
 
@@ -526,33 +548,33 @@ Dropdown component for switching between tenants. Integrates with `AuthProvider`
526
548
 
527
549
  #### Props (`TenantSelectorProps`)
528
550
 
529
- | Prop | Type | Default | Description |
530
- |------|------|---------|-------------|
531
- | `tenants` | `UserTenantMembership[]` | From context | Override tenant list |
532
- | `currentTenantId` | `string \| null` | From context | Override current tenant |
533
- | `onSelect` | `(tenantId: string) => void` | `auth.switchToTenant` | Custom selection handler |
534
- | `styles` | `TenantSelectorStyles` | See below | Override inline styles |
535
- | `className` | `string` | — | CSS class for root element |
536
- | `dropdownClassName` | `string` | — | CSS class for dropdown |
537
- | `itemClassName` | `string` | — | CSS class for each item |
538
- | `renderItem` | `(tenant, isSelected) => ReactNode` | Default renderer | Custom item renderer |
539
- | `placeholder` | `string` | `'Select tenant'` | Placeholder when no tenant selected |
540
- | `disabled` | `boolean` | `false` | Disable the selector |
541
- | `showCurrentTenant` | `boolean` | `true` | Show name when only 1 tenant |
551
+ | Prop | Type | Default | Description |
552
+ | ------------------- | ----------------------------------- | --------------------- | ----------------------------------- |
553
+ | `tenants` | `UserTenantMembership[]` | From context | Override tenant list |
554
+ | `currentTenantId` | `string \| null` | From context | Override current tenant |
555
+ | `onSelect` | `(tenantId: string) => void` | `auth.switchToTenant` | Custom selection handler |
556
+ | `styles` | `TenantSelectorStyles` | See below | Override inline styles |
557
+ | `className` | `string` | — | CSS class for root element |
558
+ | `dropdownClassName` | `string` | — | CSS class for dropdown |
559
+ | `itemClassName` | `string` | — | CSS class for each item |
560
+ | `renderItem` | `(tenant, isSelected) => ReactNode` | Default renderer | Custom item renderer |
561
+ | `placeholder` | `string` | `'Select tenant'` | Placeholder when no tenant selected |
562
+ | `disabled` | `boolean` | `false` | Disable the selector |
563
+ | `showCurrentTenant` | `boolean` | `true` | Show name when only 1 tenant |
542
564
 
543
565
  #### Styles (`TenantSelectorStyles`)
544
566
 
545
- | Key | Targets |
546
- |-----|---------|
547
- | `wrapper` | Root `<div>` (position: relative) |
548
- | `button` | Trigger button |
567
+ | Key | Targets |
568
+ | ---------------- | ------------------------------------------------- |
569
+ | `wrapper` | Root `<div>` (position: relative) |
570
+ | `button` | Trigger button |
549
571
  | `buttonDisabled` | Disabled button state (merged on top of `button`) |
550
- | `dropdown` | Dropdown menu container |
551
- | `item` | Each tenant item |
552
- | `itemSelected` | Selected tenant item (merged on top of `item`) |
553
- | `itemHover` | Hover state for items |
554
- | `itemRole` | Role badge next to tenant name |
555
- | `arrow` | Arrow indicator (▲/▼) |
572
+ | `dropdown` | Dropdown menu container |
573
+ | `item` | Each tenant item |
574
+ | `itemSelected` | Selected tenant item (merged on top of `item`) |
575
+ | `itemHover` | Hover state for items |
576
+ | `itemRole` | Role badge next to tenant name |
577
+ | `arrow` | Arrow indicator (▲/▼) |
556
578
 
557
579
  ---
558
580
 
@@ -570,13 +592,13 @@ Conditionally renders content based on permissions and/or roles.
570
592
  </Protected>
571
593
  ```
572
594
 
573
- | Prop | Type | Default | Description |
574
- |------|------|---------|-------------|
575
- | `requiredPermissions` | `string[]` | — | Required permissions |
576
- | `requiredRole` | `string` | — | Required user role |
577
- | `requireAll` | `boolean` | `true` | All permissions required? |
578
- | `fallback` | `ReactNode` | `null` | Shown when access denied |
579
- | `onUnauthorized` | `() => void` | — | Callback on denial |
595
+ | Prop | Type | Default | Description |
596
+ | --------------------- | ------------ | ------- | ------------------------- |
597
+ | `requiredPermissions` | `string[]` | — | Required permissions |
598
+ | `requiredRole` | `string` | — | Required user role |
599
+ | `requireAll` | `boolean` | `true` | All permissions required? |
600
+ | `fallback` | `ReactNode` | `null` | Shown when access denied |
601
+ | `onUnauthorized` | `() => void` | — | Callback on denial |
580
602
 
581
603
  ---
582
604
 
@@ -648,7 +670,7 @@ import { Eye, EyeOff } from 'lucide-react';
648
670
  showPassword: <Eye size={16} />,
649
671
  hidePassword: <EyeOff size={16} />,
650
672
  }}
651
- />
673
+ />;
652
674
  ```
653
675
 
654
676
  ---
@@ -657,14 +679,14 @@ import { Eye, EyeOff } from 'lucide-react';
657
679
 
658
680
  ### Core Providers
659
681
 
660
- | Provider | Purpose |
661
- |----------|---------|
662
- | **AppProvider** | Application configuration (baseUrl, appId) |
663
- | **TenantProvider** | Multi-tenant detection and management |
664
- | **AuthProvider** | Authentication, session, and user data |
665
- | **FeatureFlagProvider** | Feature flag management |
666
- | **SubscriptionProvider** | Billing and subscription handling |
667
- | **RoutingProvider** | Zone-based routing (RFC-005) |
682
+ | Provider | Purpose |
683
+ | ------------------------ | ------------------------------------------ |
684
+ | **AppProvider** | Application configuration (baseUrl, appId) |
685
+ | **TenantProvider** | Multi-tenant detection and management |
686
+ | **AuthProvider** | Authentication, session, and user data |
687
+ | **FeatureFlagProvider** | Feature flag management |
688
+ | **SubscriptionProvider** | Billing and subscription handling |
689
+ | **RoutingProvider** | Zone-based routing (RFC-005) |
668
690
 
669
691
  ### Permission System
670
692
 
@@ -683,30 +705,38 @@ reports:read - View reports
683
705
  - [🔧 Advanced Usage](./docs/advanced-usage.md)
684
706
  - [📋 API Reference](./docs/api-reference.md)
685
707
  - [🎯 Examples](./docs/examples.md)
686
- - [✨ Magic Link Guide](./MAGIC_LINK_USAGE.md)
708
+ - [✨ Magic Link Guide](./docs/magic-link-usage.md)
687
709
  - [🛣️ Zone Routing](./docs/ZONE_ROUTING.md)
710
+ - [🔒 Security & Threat Model](./docs/security.md)
711
+ - [🚚 Migration to v2.31](./docs/migration-v2.31.md)
688
712
  - [🤝 Contributing](./docs/contributing.md)
689
713
 
690
- ## 🎮 Demo Application
714
+ ## 🎮 Example Application
715
+
716
+ A complete example application is included in the `example/` directory. It's a Vite-based React app with **two areas**:
691
717
 
692
- A complete demo application is included in the `example/` directory:
718
+ - **`/demo/*`** real feature pages (login, signup, magic link, tenant switching, subscription, zone routing, cookie session, standalone auth).
719
+ - **`/labs/*`** — inspection labs for the internal state of each provider and service (useful while developing against the library).
693
720
 
694
721
  ```bash
695
722
  cd example
696
723
  yarn install
697
- yarn start
724
+ yarn dev
698
725
  ```
699
726
 
700
- The demo showcases:
701
- - **Traditional Authentication** - Email/phone + password login
702
- - **Magic Link Authentication** - Passwordless login with automatic verification
703
- - **User Registration** - Signup with email/phone support
704
- - **Password Recovery** - Reset password functionality
705
- - **Role-based Dashboard** - Different views based on user roles
706
- - **Permission Testing** - Interactive permission system testing
707
- - **Protected Routes** - Route-level access control
708
- - **Feature Flag Usage** - Dynamic feature toggling
709
- - **Multi-tenant Support** - Tenant switching and management
727
+ The example showcases:
728
+
729
+ - **Traditional Authentication** email/phone + password login
730
+ - **Magic Link Authentication** passwordless login with automatic verification
731
+ - **User Registration** signup with email/phone support
732
+ - **Password Recovery** request + reset flows
733
+ - **Role-based Dashboard** different views based on user roles
734
+ - **Permission Testing** interactive `Protected` / `ProtectedRoute` demos
735
+ - **Zone Routing** declarative route access control (RFC-005)
736
+ - **Feature Flags** dynamic feature toggling
737
+ - **Multi-tenant Support** — tenant switching and management
738
+ - **Standalone AuthProvider** — auth-only setup without App/Tenant providers
739
+ - **Cross-subdomain Cookie Session** — `enableCookieSession` walkthrough
710
740
 
711
741
  ## 🛠️ Development
712
742
 
@@ -769,16 +799,49 @@ REACT_APP_TENANT_MODE=subdomain
769
799
  ```tsx
770
800
  // AppProvider Config
771
801
  interface AppConfig {
772
- baseUrl: string; // API base URL
773
- appId: string; // Application identifier
802
+ baseUrl: string; // API base URL
803
+ appId: string; // Application identifier
804
+ cache?: {
805
+ enabled?: boolean; // Default: true
806
+ ttl?: number; // Cache TTL in ms, default: 5 min
807
+ storageKey?: string; // Default: `app_cache_{appId}`
808
+ };
774
809
  }
775
810
 
776
811
  // TenantProvider Config
777
812
  interface TenantConfig {
778
- tenantMode: 'subdomain' | 'selector' | 'fixed' | 'optional';
779
- selectorParam?: string; // For 'selector' mode
780
- fixedTenantSlug?: string; // For 'fixed' mode
781
- initialTenant?: string; // Initial tenant value
813
+ tenantMode?: 'subdomain' | 'selector' | 'fixed';
814
+ fixedTenantSlug?: string; // Required when tenantMode is 'fixed'
815
+ baseDomain?: string; // Base domain for subdomain mode (e.g. 'acme.app')
816
+ selectorParam?: string; // Default: 'tenant', used in 'selector' mode
817
+ cache?: {
818
+ enabled?: boolean; // Default: true
819
+ ttl?: number; // Cache TTL in ms, default: 5 min
820
+ storageKey?: string; // Default: `tenant_cache_{tenantSlug}`
821
+ };
822
+ initialTenant?: PublicTenantInfo; // SSR support
823
+ }
824
+
825
+ // AuthProvider Config (all fields optional)
826
+ interface AuthConfig {
827
+ // Standalone mode — required if AppProvider is not used
828
+ baseUrl?: string;
829
+ appId?: string;
830
+
831
+ // Session lifecycle
832
+ onSessionExpired?: (error: SessionExpiredError) => void;
833
+ refreshQueueTimeout?: number; // Default: 10_000 ms
834
+ proactiveRefreshMargin?: number; // Default: 60_000 ms before expiry
835
+
836
+ // Cross-subdomain cookie session (see "Security & Resilience" below)
837
+ enableCookieSession?: boolean; // Default: false
838
+
839
+ // Multi-tenant UX
840
+ autoSwitchSingleTenant?: boolean; // Auto-switch when user has exactly one tenant
841
+ onTenantSelectionRequired?: (tenants: UserTenantMembership[]) => void;
842
+
843
+ // Initial roles (avoid extra fetch on mount)
844
+ initialRoles?: Role[];
782
845
  }
783
846
  ```
784
847
 
@@ -802,13 +865,37 @@ yarn test:coverage
802
865
  - **Optimized re-renders** - Minimal React re-renders
803
866
  - **Caching** - Intelligent caching of API responses
804
867
 
805
- ## 🔒 Security
868
+ ## 🔒 Security & Resilience
869
+
870
+ ### Token lifecycle
871
+
872
+ - **JWT access tokens** with automatic refresh and proactive renewal (60s margin by default).
873
+ - **Refresh queueing** — concurrent requests during refresh share a single in-flight request; no duplicate rotations.
874
+ - **Session generation tracking** — refresh attempts issued before a logout are discarded, preventing stale tokens from resurrecting a session.
875
+ - **Circuit breaker** — three consecutive background refresh failures transition the session to expired, preventing infinite retry loops.
876
+ - **Token reuse / revocation** — classified as fatal; session is cleared immediately to contain rotation-chain compromise.
877
+
878
+ ### Multi-tab safety
879
+
880
+ The `SessionManager` is a singleton per `(baseUrl, tenantSlug)` pair and coordinates across tabs using the **Web Locks API**. Two tabs opening at the same time cannot issue duplicate refresh calls, and after a refresh all tabs converge on the same access token via shared storage.
881
+
882
+ ### Cross-subdomain cookie session (`enableCookieSession`)
883
+
884
+ For SSO between subdomains (e.g. `www.example.com` ↔ `app.example.com`), set `enableCookieSession: true` in `AuthConfig`. When enabled:
885
+
886
+ - Refresh requests are sent with `credentials: 'include'`, so the backend's HttpOnly refresh cookie flows automatically.
887
+ - On app load, `SessionManager` attempts a cookie-based restore before requiring interactive login.
888
+ - No refresh token is ever written to `localStorage` — the access token lives in memory, the refresh token lives only in the HttpOnly cookie.
889
+
890
+ This replaces the previous, insecure `_auth=<token>` URL transfer mechanism (removed in **v2.31**). See [Migration to v2.31](./docs/migration-v2.31.md) for details.
891
+
892
+ ### Permissions
893
+
894
+ - `resource:action` format, validated on both client and server.
895
+ - `Protected` / `ProtectedRoute` / `ZoneRoute` components block rendering when permissions are missing.
896
+ - Console output is suppressed in production and test environments.
806
897
 
807
- - **JWT tokens** with automatic refresh and proactive renewal
808
- - **Secure token storage** with configurable backends
809
- - **Session generation tracking** to prevent stale token usage
810
- - **Permission validation** on both client and server
811
- - **Console output suppressed** in production and test environments
898
+ For a full threat model and list of fixed vulnerabilities, see [docs/security.md](./docs/security.md).
812
899
 
813
900
  ## 🌐 Browser Support
814
901