@douvery/auth 0.2.1 → 0.3.1

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
@@ -29,6 +29,9 @@
29
29
  - ðŸ“Ą **Event System** - Subscribe to auth events (login, logout, token refresh)
30
30
  - 🌐 **SSR Compatible** - Works with server-side rendering
31
31
  - ðŸŠķ **Lightweight** - ~23KB core, framework adapters add minimal overhead
32
+ - 🧭 **Auth Navigation** - Built-in helpers for all auth flows (register, recover, verify, switch account, etc.)
33
+ - 🔗 **URL Builders** - Generate auth URLs for `<a>` tags without programmatic redirect
34
+ - 🔑 **Token Revocation** - Revoke access/refresh tokens via RFC 7009
32
35
 
33
36
  ---
34
37
 
@@ -197,6 +200,31 @@ fetch('/api/protected', {
197
200
 
198
201
  // 7. Logout
199
202
  await auth.logout();
203
+
204
+ // 8. Navigation helpers — redirect to any auth flow
205
+ auth.selectAccount({ returnTo: '/dashboard' });
206
+ auth.addAccount({ loginHint: 'other@email.com' });
207
+ auth.register({ email: 'new@email.com' });
208
+ auth.recoverAccount({ email: 'user@email.com' });
209
+ auth.verifyAccount();
210
+ auth.upgradeAccount();
211
+ auth.setupPasskey();
212
+ auth.setupAddress();
213
+
214
+ // 9. URL builders (don't redirect, useful for <a> tags)
215
+ const url = auth.buildSelectAccountUrl({ returnTo: '/home' });
216
+ console.log(url.url); // "https://auth.douvery.com/select-account?continue=/home"
217
+ url.redirect(); // Navigate to the URL
218
+ url.open(); // Open in new tab
219
+
220
+ // 10. Revoke a token
221
+ await auth.revokeToken();
222
+ await auth.revokeToken({ tokenTypeHint: 'refresh_token' });
223
+
224
+ // 11. Session status helpers
225
+ auth.isSessionExpired(); // true/false
226
+ auth.needsEmailVerification(); // true/false
227
+ auth.isGuestAccount(); // true/false
200
228
  ```
201
229
 
202
230
  ---
@@ -280,6 +308,106 @@ await auth.logout({
280
308
  });
281
309
  ```
282
310
 
311
+ ### Navigation Options
312
+
313
+ All navigation methods accept a common base of options:
314
+
315
+ ```typescript
316
+ interface AuthNavigationOptions {
317
+ returnTo?: string; // URL to return to after the action
318
+ clientId?: string; // OAuth client_id for branded experiences
319
+ openInNewTab?: boolean; // Open in new tab instead of redirect
320
+ }
321
+ ```
322
+
323
+ ```typescript
324
+ // Select / switch account
325
+ auth.selectAccount({
326
+ returnTo: '/dashboard',
327
+ loginHint: 'user@email.com', // Pre-select a specific account
328
+ });
329
+
330
+ // Add another account (multi-session)
331
+ auth.addAccount({
332
+ loginHint: 'another@email.com',
333
+ });
334
+
335
+ // Register a new account
336
+ auth.register({
337
+ email: 'new@email.com',
338
+ firstName: 'John',
339
+ lastName: 'Doe',
340
+ uiLocales: 'es',
341
+ });
342
+
343
+ // Recover account (forgot password)
344
+ auth.recoverAccount({
345
+ email: 'user@email.com',
346
+ });
347
+
348
+ // Verify account (email verification)
349
+ auth.verifyAccount({
350
+ email: 'user@email.com',
351
+ });
352
+
353
+ // Upgrade guest account to full account
354
+ auth.upgradeAccount({
355
+ scopes: ['profile', 'email'],
356
+ });
357
+
358
+ // Setup passkey
359
+ auth.setupPasskey({ returnTo: '/settings' });
360
+
361
+ // Setup address
362
+ auth.setupAddress({ returnTo: '/checkout' });
363
+ ```
364
+
365
+ ### URL Builders
366
+
367
+ Every navigation method has a corresponding `build*Url()` method that returns an `AuthUrl` object without redirecting. Useful for `<a>` tags or custom navigation logic:
368
+
369
+ ```typescript
370
+ const url = auth.buildRegisterUrl({ email: 'hint@test.com' });
371
+
372
+ url.url; // "https://auth.douvery.com/register?email=hint%40test.com"
373
+ url.redirect(); // window.location.href = url
374
+ url.open(); // window.open(url, '_blank')
375
+ ```
376
+
377
+ | Builder | Path |
378
+ |---|---|
379
+ | `buildLoginUrl()` | `/login` |
380
+ | `buildLogoutUrl()` | `/logout` |
381
+ | `buildSelectAccountUrl()` | `/select-account` |
382
+ | `buildAddAccountUrl()` | `/login?add_account=true` |
383
+ | `buildRegisterUrl()` | `/register` |
384
+ | `buildRecoverAccountUrl()` | `/recover-account` |
385
+ | `buildVerifyAccountUrl()` | `/verify-account` |
386
+ | `buildUpgradeAccountUrl()` | `/upgrade-account` |
387
+ | `buildSetupPasskeyUrl()` | `/setup-passkey` |
388
+ | `buildSetupAddressUrl()` | `/setup-address` |
389
+
390
+ ### Token Revocation
391
+
392
+ ```typescript
393
+ // Revoke current access token
394
+ await auth.revokeToken();
395
+
396
+ // Revoke a specific token
397
+ await auth.revokeToken({
398
+ token: 'specific-token-value',
399
+ tokenTypeHint: 'refresh_token',
400
+ });
401
+ ```
402
+
403
+ ### Session Status Helpers
404
+
405
+ ```typescript
406
+ auth.isSessionExpired(); // true if access token is expired
407
+ auth.needsEmailVerification(); // true if email is not verified
408
+ auth.isGuestAccount(); // true if account type is guest
409
+ ```
410
+
283
411
  ### Auth State
284
412
 
285
413
  ```typescript
@@ -330,7 +458,47 @@ type AuthEvent =
330
458
  | `useUser()` | Current user or null |
331
459
  | `useIsAuthenticated()` | Boolean authentication status |
332
460
  | `useAccessToken()` | `{ accessToken, getAccessToken }` |
333
- | `useAuthActions()` | `{ login, logout, isLoading }` |
461
+ | `useAuthActions()` | All auth actions (see below) |
462
+ | `useAuthUrls()` | URL builders for all auth pages |
463
+ | `useSessionStatus()` | `{ isExpired, needsVerification, isGuest }` |
464
+
465
+ #### `useAuthActions()` — Full list
466
+
467
+ ```tsx
468
+ const {
469
+ login, // (options?: LoginOptions) => Promise<void>
470
+ logout, // (options?: LogoutOptions) => Promise<void>
471
+ selectAccount, // (options?: SelectAccountOptions) => void
472
+ addAccount, // (options?: AddAccountOptions) => void
473
+ register, // (options?: RegisterOptions) => void
474
+ recoverAccount, // (options?: RecoverAccountOptions) => void
475
+ verifyAccount, // (options?: VerifyAccountOptions) => void
476
+ upgradeAccount, // (options?: UpgradeAccountOptions) => void
477
+ setupPasskey, // (options?: SetupPasskeyOptions) => void
478
+ setupAddress, // (options?: SetupAddressOptions) => void
479
+ revokeToken, // (options?: RevokeTokenOptions) => Promise<void>
480
+ isLoading, // boolean
481
+ } = useAuthActions();
482
+ ```
483
+
484
+ #### `useAuthUrls()` — URL builders for links
485
+
486
+ ```tsx
487
+ const urls = useAuthUrls();
488
+
489
+ <a href={urls.registerUrl().url}>Create account</a>
490
+ <a href={urls.recoverAccountUrl({ email: user.email }).url}>Forgot password?</a>
491
+ <a href={urls.selectAccountUrl().url}>Switch account</a>
492
+ ```
493
+
494
+ #### `useSessionStatus()` — Reactive status
495
+
496
+ ```tsx
497
+ const { isExpired, needsVerification, isGuest } = useSessionStatus();
498
+
499
+ {needsVerification && <Banner>Please verify your email</Banner>}
500
+ {isGuest && <button onClick={() => upgradeAccount()}>Upgrade account</button>}
501
+ ```
334
502
 
335
503
  ### DouveryAuthProvider Props
336
504
 
@@ -354,7 +522,36 @@ interface DouveryAuthProviderProps {
354
522
  | `useDouveryAuth()` | Context | Full context with signals and client |
355
523
  | `useUser()` | `Signal<User \| null>` | Reactive user signal |
356
524
  | `useIsAuthenticated()` | `Signal<boolean>` | Reactive auth status |
357
- | `useAuthActions()` | `{ login, logout, isLoading }` | Auth actions |
525
+ | `useAuthActions()` | All actions + `isLoading` | Auth actions (same as React) |
526
+ | `useAuthUrls()` | URL builders | Build URLs for auth pages |
527
+ | `useSessionStatus()` | `{ isExpired, needsVerification, isGuest }` | Reactive session signals |
528
+
529
+ #### Qwik example with navigation
530
+
531
+ ```tsx
532
+ import { component$ } from '@builder.io/qwik';
533
+ import { useAuthActions, useSessionStatus } from '@douvery/auth/qwik';
534
+
535
+ export const AccountMenu = component$(() => {
536
+ const { selectAccount, addAccount, recoverAccount, upgradeAccount } = useAuthActions();
537
+ const { isGuest, needsVerification } = useSessionStatus();
538
+
539
+ return (
540
+ <div>
541
+ <button onClick$={() => selectAccount({ returnTo: window.location.href })}>
542
+ Switch account
543
+ </button>
544
+ <button onClick$={() => addAccount()}>Add account</button>
545
+ {isGuest.value && (
546
+ <button onClick$={() => upgradeAccount()}>Upgrade account</button>
547
+ )}
548
+ {needsVerification.value && (
549
+ <button onClick$={() => verifyAccount()}>Verify email</button>
550
+ )}
551
+ </div>
552
+ );
553
+ });
554
+ ```
358
555
 
359
556
  ---
360
557
 
package/dist/index.d.ts CHANGED
@@ -207,6 +207,64 @@ interface LogoutOptions {
207
207
  /** Only clear local session, don't redirect @default false */
208
208
  localOnly?: boolean;
209
209
  }
210
+ /** Base options for all auth navigation redirects */
211
+ interface AuthNavigationOptions {
212
+ /** URL to return to after the action completes */
213
+ returnTo?: string;
214
+ /** OAuth client_id for branded experiences */
215
+ clientId?: string;
216
+ /** Open in a new tab/window instead of redirecting */
217
+ openInNewTab?: boolean;
218
+ }
219
+ interface SelectAccountOptions extends AuthNavigationOptions {
220
+ /** Pre-select a specific email/account */
221
+ loginHint?: string;
222
+ }
223
+ interface RegisterOptions extends AuthNavigationOptions {
224
+ /** Pre-fill email on register form */
225
+ email?: string;
226
+ /** Pre-fill first name */
227
+ firstName?: string;
228
+ /** Pre-fill last name */
229
+ lastName?: string;
230
+ /** UI locale preference */
231
+ uiLocales?: string;
232
+ }
233
+ interface RecoverAccountOptions extends AuthNavigationOptions {
234
+ /** Pre-fill email for recovery */
235
+ email?: string;
236
+ }
237
+ interface VerifyAccountOptions extends AuthNavigationOptions {
238
+ /** Email to verify */
239
+ email?: string;
240
+ }
241
+ interface UpgradeAccountOptions extends AuthNavigationOptions {
242
+ /** Scopes to request on upgrade */
243
+ scopes?: string[];
244
+ }
245
+ interface SetupPasskeyOptions extends AuthNavigationOptions {
246
+ }
247
+ interface SetupAddressOptions extends AuthNavigationOptions {
248
+ }
249
+ interface AddAccountOptions extends AuthNavigationOptions {
250
+ /** Pre-fill email hint for the new account */
251
+ loginHint?: string;
252
+ }
253
+ interface RevokeTokenOptions {
254
+ /** Specific token to revoke. If not provided, revokes current access token */
255
+ token?: string;
256
+ /** Token type hint: "access_token" or "refresh_token" */
257
+ tokenTypeHint?: "access_token" | "refresh_token";
258
+ }
259
+ /** Result of URL builder methods (non-redirecting) */
260
+ interface AuthUrl {
261
+ /** The full URL */
262
+ url: string;
263
+ /** Open the URL via redirect */
264
+ redirect: () => void;
265
+ /** Open the URL in a new tab */
266
+ open: () => Window | null;
267
+ }
210
268
 
211
269
  /**
212
270
  * @douvery/auth - Auth Client
@@ -236,7 +294,55 @@ declare class DouveryAuthClient {
236
294
  refreshTokens(): Promise<TokenInfo>;
237
295
  /** Get current access token (auto-refreshes if needed) */
238
296
  getAccessToken(): Promise<string | null>;
297
+ /** Redirect to select/switch account */
298
+ selectAccount(options?: SelectAccountOptions): void;
299
+ /** Build select-account URL without redirecting */
300
+ buildSelectAccountUrl(options?: SelectAccountOptions): AuthUrl;
301
+ /** Redirect to add another account (multi-session) */
302
+ addAccount(options?: AddAccountOptions): void;
303
+ /** Build add-account URL without redirecting */
304
+ buildAddAccountUrl(options?: AddAccountOptions): AuthUrl;
305
+ /** Redirect to register a new account */
306
+ register(options?: RegisterOptions): void;
307
+ /** Build register URL without redirecting */
308
+ buildRegisterUrl(options?: RegisterOptions): AuthUrl;
309
+ /** Redirect to recover account (forgot password) */
310
+ recoverAccount(options?: RecoverAccountOptions): void;
311
+ /** Build recover-account URL without redirecting */
312
+ buildRecoverAccountUrl(options?: RecoverAccountOptions): AuthUrl;
313
+ /** Redirect to verify account (email verification) */
314
+ verifyAccount(options?: VerifyAccountOptions): void;
315
+ /** Build verify-account URL without redirecting */
316
+ buildVerifyAccountUrl(options?: VerifyAccountOptions): AuthUrl;
317
+ /** Redirect to upgrade account (guest → full account) */
318
+ upgradeAccount(options?: UpgradeAccountOptions): void;
319
+ /** Build upgrade-account URL without redirecting */
320
+ buildUpgradeAccountUrl(options?: UpgradeAccountOptions): AuthUrl;
321
+ /** Redirect to passkey setup */
322
+ setupPasskey(options?: SetupPasskeyOptions): void;
323
+ /** Build setup-passkey URL without redirecting */
324
+ buildSetupPasskeyUrl(options?: SetupPasskeyOptions): AuthUrl;
325
+ /** Redirect to address setup */
326
+ setupAddress(options?: SetupAddressOptions): void;
327
+ /** Build setup-address URL without redirecting */
328
+ buildSetupAddressUrl(options?: SetupAddressOptions): AuthUrl;
329
+ /** Build a login URL without redirecting (useful for links/buttons) */
330
+ buildLoginUrl(options?: LoginOptions): AuthUrl;
331
+ /** Build a logout URL without redirecting */
332
+ buildLogoutUrl(options?: LogoutOptions): AuthUrl;
333
+ /** Revoke a token (access or refresh) */
334
+ revokeToken(options?: RevokeTokenOptions): Promise<void>;
335
+ /** Check if the user's session is expired */
336
+ isSessionExpired(): boolean;
337
+ /** Check if user needs email verification */
338
+ needsEmailVerification(): boolean;
339
+ /** Check if user is a guest account */
340
+ isGuestAccount(): boolean;
239
341
  private tokenSetToInfo;
342
+ /** Build an AuthUrl object for a given path and params */
343
+ private createAuthUrl;
344
+ /** Navigate to an auth URL, respecting openInNewTab option */
345
+ private navigate;
240
346
  private fetchUser;
241
347
  private extractUserFromIdToken;
242
348
  private normalizeUser;
@@ -349,4 +455,4 @@ declare class TokenManager {
349
455
  clearAll(): Promise<void>;
350
456
  }
351
457
 
352
- export { AuthError, type AuthErrorCode, type AuthEvent, type AuthEventHandler, type AuthState, type AuthStatus, type CallbackResult, CookieStorage, type DecodedIdToken, DouveryAuthClient, type DouveryAuthConfig, LocalStorage, type LoginOptions, type LogoutOptions, MemoryStorage, type OIDCDiscovery, type PKCEPair, STORAGE_KEYS, SessionStorage, type StorageKeys, type TokenInfo, TokenManager, type TokenSet, type TokenStorage, type User, base64UrlDecode, base64UrlEncode, createDouveryAuth, createStorage, decodeJWT, generateCodeChallenge, generateCodeVerifier, generateNonce, generatePKCEPair, generateState, getTokenExpiration, isTokenExpired, verifyCodeChallenge };
458
+ export { type AddAccountOptions, AuthError, type AuthErrorCode, type AuthEvent, type AuthEventHandler, type AuthNavigationOptions, type AuthState, type AuthStatus, type AuthUrl, type CallbackResult, CookieStorage, type DecodedIdToken, DouveryAuthClient, type DouveryAuthConfig, LocalStorage, type LoginOptions, type LogoutOptions, MemoryStorage, type OIDCDiscovery, type PKCEPair, type RecoverAccountOptions, type RegisterOptions, type RevokeTokenOptions, STORAGE_KEYS, type SelectAccountOptions, SessionStorage, type SetupAddressOptions, type SetupPasskeyOptions, type StorageKeys, type TokenInfo, TokenManager, type TokenSet, type TokenStorage, type UpgradeAccountOptions, type User, type VerifyAccountOptions, base64UrlDecode, base64UrlEncode, createDouveryAuth, createStorage, decodeJWT, generateCodeChallenge, generateCodeVerifier, generateNonce, generatePKCEPair, generateState, getTokenExpiration, isTokenExpired, verifyCodeChallenge };
package/dist/index.js CHANGED
@@ -636,6 +636,171 @@ var DouveryAuthClient = class {
636
636
  }
637
637
  return tokens.accessToken;
638
638
  }
639
+ // ============================================
640
+ // Navigation Methods
641
+ // ============================================
642
+ /** Redirect to select/switch account */
643
+ selectAccount(options = {}) {
644
+ const url = this.buildSelectAccountUrl(options);
645
+ this.navigate(url, options);
646
+ }
647
+ /** Build select-account URL without redirecting */
648
+ buildSelectAccountUrl(options = {}) {
649
+ const params = new URLSearchParams();
650
+ if (options.returnTo) params.set("continue", options.returnTo);
651
+ if (options.clientId) params.set("client_id", options.clientId);
652
+ if (options.loginHint) params.set("login_hint", options.loginHint);
653
+ return this.createAuthUrl("/select-account", params);
654
+ }
655
+ /** Redirect to add another account (multi-session) */
656
+ addAccount(options = {}) {
657
+ const url = this.buildAddAccountUrl(options);
658
+ this.navigate(url, options);
659
+ }
660
+ /** Build add-account URL without redirecting */
661
+ buildAddAccountUrl(options = {}) {
662
+ const params = new URLSearchParams({ add_account: "true" });
663
+ if (options.returnTo) params.set("continue", options.returnTo);
664
+ if (options.clientId) params.set("client_id", options.clientId);
665
+ if (options.loginHint) params.set("login_hint", options.loginHint);
666
+ return this.createAuthUrl("/login", params);
667
+ }
668
+ /** Redirect to register a new account */
669
+ register(options = {}) {
670
+ const url = this.buildRegisterUrl(options);
671
+ this.navigate(url, options);
672
+ }
673
+ /** Build register URL without redirecting */
674
+ buildRegisterUrl(options = {}) {
675
+ const params = new URLSearchParams();
676
+ if (options.returnTo) params.set("continue", options.returnTo);
677
+ if (options.clientId) params.set("client_id", options.clientId);
678
+ if (options.email) params.set("email", options.email);
679
+ if (options.firstName) params.set("first_name", options.firstName);
680
+ if (options.lastName) params.set("last_name", options.lastName);
681
+ if (options.uiLocales) params.set("ui_locales", options.uiLocales);
682
+ return this.createAuthUrl("/register", params);
683
+ }
684
+ /** Redirect to recover account (forgot password) */
685
+ recoverAccount(options = {}) {
686
+ const url = this.buildRecoverAccountUrl(options);
687
+ this.navigate(url, options);
688
+ }
689
+ /** Build recover-account URL without redirecting */
690
+ buildRecoverAccountUrl(options = {}) {
691
+ const params = new URLSearchParams();
692
+ if (options.returnTo) params.set("continue", options.returnTo);
693
+ if (options.clientId) params.set("client_id", options.clientId);
694
+ if (options.email) params.set("email", options.email);
695
+ return this.createAuthUrl("/recover-account", params);
696
+ }
697
+ /** Redirect to verify account (email verification) */
698
+ verifyAccount(options = {}) {
699
+ const url = this.buildVerifyAccountUrl(options);
700
+ this.navigate(url, options);
701
+ }
702
+ /** Build verify-account URL without redirecting */
703
+ buildVerifyAccountUrl(options = {}) {
704
+ const params = new URLSearchParams();
705
+ if (options.returnTo) params.set("continue", options.returnTo);
706
+ if (options.clientId) params.set("client_id", options.clientId);
707
+ if (options.email) params.set("email", options.email);
708
+ return this.createAuthUrl("/verify-account", params);
709
+ }
710
+ /** Redirect to upgrade account (guest → full account) */
711
+ upgradeAccount(options = {}) {
712
+ const url = this.buildUpgradeAccountUrl(options);
713
+ this.navigate(url, options);
714
+ }
715
+ /** Build upgrade-account URL without redirecting */
716
+ buildUpgradeAccountUrl(options = {}) {
717
+ const params = new URLSearchParams();
718
+ if (options.returnTo) params.set("continue", options.returnTo);
719
+ if (options.clientId) params.set("client_id", options.clientId);
720
+ if (options.scopes?.length) params.set("scope", options.scopes.join(" "));
721
+ return this.createAuthUrl("/upgrade-account", params);
722
+ }
723
+ /** Redirect to passkey setup */
724
+ setupPasskey(options = {}) {
725
+ const url = this.buildSetupPasskeyUrl(options);
726
+ this.navigate(url, options);
727
+ }
728
+ /** Build setup-passkey URL without redirecting */
729
+ buildSetupPasskeyUrl(options = {}) {
730
+ const params = new URLSearchParams();
731
+ if (options.returnTo) params.set("continue", options.returnTo);
732
+ if (options.clientId) params.set("client_id", options.clientId);
733
+ return this.createAuthUrl("/setup-passkey", params);
734
+ }
735
+ /** Redirect to address setup */
736
+ setupAddress(options = {}) {
737
+ const url = this.buildSetupAddressUrl(options);
738
+ this.navigate(url, options);
739
+ }
740
+ /** Build setup-address URL without redirecting */
741
+ buildSetupAddressUrl(options = {}) {
742
+ const params = new URLSearchParams();
743
+ if (options.returnTo) params.set("continue", options.returnTo);
744
+ if (options.clientId) params.set("client_id", options.clientId);
745
+ return this.createAuthUrl("/setup-address", params);
746
+ }
747
+ /** Build a login URL without redirecting (useful for links/buttons) */
748
+ buildLoginUrl(options = {}) {
749
+ const params = new URLSearchParams();
750
+ if (options.returnTo) params.set("continue", options.returnTo);
751
+ if (options.loginHint) params.set("email", options.loginHint);
752
+ if (options.prompt) params.set("prompt", options.prompt);
753
+ if (options.uiLocales) params.set("ui_locales", options.uiLocales);
754
+ return this.createAuthUrl("/login", params);
755
+ }
756
+ /** Build a logout URL without redirecting */
757
+ buildLogoutUrl(options = {}) {
758
+ const params = new URLSearchParams();
759
+ if (options.returnTo) params.set("continue", options.returnTo);
760
+ return this.createAuthUrl("/logout", params);
761
+ }
762
+ /** Revoke a token (access or refresh) */
763
+ async revokeToken(options = {}) {
764
+ this.log("Revoking token...");
765
+ const token = options.token ?? (await this.tokenManager.getTokens())?.accessToken;
766
+ if (!token) {
767
+ throw new AuthError("invalid_token", "No token to revoke");
768
+ }
769
+ const revokeUrl = `${this.config.issuer}/oauth/revoke`;
770
+ const body = new URLSearchParams({
771
+ token,
772
+ client_id: this.config.clientId
773
+ });
774
+ if (options.tokenTypeHint) {
775
+ body.set("token_type_hint", options.tokenTypeHint);
776
+ }
777
+ const response = await fetch(revokeUrl, {
778
+ method: "POST",
779
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
780
+ body
781
+ });
782
+ if (!response.ok) {
783
+ const error = await response.json().catch(() => ({}));
784
+ throw new AuthError(
785
+ error.error ?? "server_error",
786
+ error.error_description ?? "Token revocation failed"
787
+ );
788
+ }
789
+ this.log("Token revoked successfully");
790
+ }
791
+ /** Check if the user's session is expired */
792
+ isSessionExpired() {
793
+ if (!this.state.tokens) return true;
794
+ return isTokenExpired(this.state.tokens.accessToken);
795
+ }
796
+ /** Check if user needs email verification */
797
+ needsEmailVerification() {
798
+ return this.state.user?.emailVerified === false;
799
+ }
800
+ /** Check if user is a guest account */
801
+ isGuestAccount() {
802
+ return this.state.user?.accountType === "guest";
803
+ }
639
804
  tokenSetToInfo(tokenSet) {
640
805
  return {
641
806
  accessToken: tokenSet.access_token,
@@ -646,6 +811,28 @@ var DouveryAuthClient = class {
646
811
  scope: tokenSet.scope?.split(" ") ?? []
647
812
  };
648
813
  }
814
+ /** Build an AuthUrl object for a given path and params */
815
+ createAuthUrl(path, params) {
816
+ const query = params.toString();
817
+ const url = `${this.config.issuer}${path}${query ? `?${query}` : ""}`;
818
+ return {
819
+ url,
820
+ redirect: () => {
821
+ window.location.href = url;
822
+ },
823
+ open: () => {
824
+ return window.open(url, "_blank");
825
+ }
826
+ };
827
+ }
828
+ /** Navigate to an auth URL, respecting openInNewTab option */
829
+ navigate(authUrl, options) {
830
+ if (options.openInNewTab) {
831
+ authUrl.open();
832
+ } else {
833
+ authUrl.redirect();
834
+ }
835
+ }
649
836
  async fetchUser(accessToken) {
650
837
  const discovery = await this.getDiscovery();
651
838
  const response = await fetch(discovery.userinfo_endpoint, {